diff --git a/engine/src/main/java/com/arcadedb/graph/GraphEngine.java b/engine/src/main/java/com/arcadedb/graph/GraphEngine.java index 6f2eef6892..f6915d8b5a 100644 --- a/engine/src/main/java/com/arcadedb/graph/GraphEngine.java +++ b/engine/src/main/java/com/arcadedb/graph/GraphEngine.java @@ -552,7 +552,10 @@ public boolean isVertexConnectedTo(final VertexInternal vertex, final Identifiab return false; } - public boolean isVertexConnectedTo(final VertexInternal vertex, final Identifiable toVertex, final Vertex.DIRECTION direction, + public boolean isVertexConnectedTo( + final VertexInternal vertex, + final Identifiable toVertex, + final Vertex.DIRECTION direction, final String edgeType) { if (toVertex == null) throw new IllegalArgumentException("Destination vertex is null"); @@ -563,13 +566,17 @@ public boolean isVertexConnectedTo(final VertexInternal vertex, final Identifiab if (edgeType == null) throw new IllegalArgumentException("Edge type is null"); - final int[] bucketFilter = vertex.getDatabase().getSchema().getType(edgeType).getBuckets(true).stream() - .mapToInt(x -> x.getFileId()).toArray(); + final int[] bucketFilter = vertex.getDatabase() + .getSchema() + .getType(edgeType) + .getBuckets(true) + .stream() + .mapToInt(x -> x.getFileId()) + .toArray(); if (direction == Vertex.DIRECTION.OUT || direction == Vertex.DIRECTION.BOTH) { final EdgeLinkedList outEdges = getEdgeHeadChunk(vertex, Vertex.DIRECTION.OUT); - if (outEdges != null && outEdges.containsVertex(toVertex.getIdentity(), bucketFilter)) - return true; + return outEdges != null && outEdges.containsVertex(toVertex.getIdentity(), bucketFilter); } if (direction == Vertex.DIRECTION.IN || direction == Vertex.DIRECTION.BOTH) { diff --git a/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgeExecutionPlanner.java b/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgeExecutionPlanner.java index f49dbbd6ca..6dca0db264 100644 --- a/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgeExecutionPlanner.java +++ b/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgeExecutionPlanner.java @@ -20,6 +20,7 @@ import com.arcadedb.database.Database; import com.arcadedb.exception.CommandSQLParsingException; +import com.arcadedb.index.TypeIndex; import com.arcadedb.query.sql.parser.CreateEdgeStatement; import com.arcadedb.query.sql.parser.Expression; import com.arcadedb.query.sql.parser.Identifier; @@ -28,8 +29,10 @@ import com.arcadedb.query.sql.parser.JsonArray; import com.arcadedb.query.sql.parser.UpdateItem; import com.arcadedb.schema.DocumentType; +import com.arcadedb.schema.EdgeType; -import java.util.*; +import java.util.ArrayList; +import java.util.List; /** * Created by luigidellaquila on 08/08/16. @@ -85,7 +88,21 @@ public InsertExecutionPlan createExecutionPlan(final CommandContext context) { handleGlobalLet(result, new Identifier("$__ARCADEDB_CREATE_EDGE_toV"), rightExpression, context); final String uniqueIndexName; - uniqueIndexName = null; + if (context.getDatabase().getSchema().existsType(targetClass.getStringValue())) { + final EdgeType type = (EdgeType) context.getDatabase().getSchema().getType(targetClass.getStringValue()); + uniqueIndexName = type.getAllIndexes(true) + + .stream() + .peek(x -> System.out.println("Index: " + x.getName())) + .filter(TypeIndex::isUnique) + .filter(x -> x.getPropertyNames().size() == 2 + && x.getPropertyNames().contains("@out") + && x.getPropertyNames().contains("@in")) + .map(TypeIndex::getName) + .findFirst() + .orElse(null); + } else + uniqueIndexName = null; result.chain( new CreateEdgesStep(targetClass, targetBucketName, uniqueIndexName, new Identifier("$__ARCADEDB_CREATE_EDGE_fromV"), diff --git a/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgesStep.java b/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgesStep.java index db889cef6f..893ec30349 100755 --- a/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgesStep.java +++ b/engine/src/main/java/com/arcadedb/query/sql/executor/CreateEdgesStep.java @@ -32,7 +32,11 @@ import com.arcadedb.index.IndexCursor; import com.arcadedb.query.sql.parser.Identifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; /** * Created by luigidellaquila on 28/11/16. @@ -58,8 +62,14 @@ public class CreateEdgesStep extends AbstractExecutionStep { private boolean initiated = false; - public CreateEdgesStep(final Identifier targetClass, final Identifier targetBucketName, final String uniqueIndex, - final Identifier fromAlias, final Identifier toAlias, final boolean unidirectional, final boolean ifNotExists, + public CreateEdgesStep( + final Identifier targetClass, + final Identifier targetBucketName, + final String uniqueIndex, + final Identifier fromAlias, + final Identifier toAlias, + final boolean unidirectional, + final boolean ifNotExists, final CommandContext context) { super(context); this.targetClass = targetClass; @@ -103,11 +113,20 @@ public Result next(final Object[] properties) { if (currentTo == null) throw new CommandExecutionException("Invalid TO vertex for edge"); - if (ifNotExists) + if (ifNotExists) { if (context.getDatabase().getGraphEngine() - .isVertexConnectedTo((VertexInternal) currentFrom, currentTo, Vertex.DIRECTION.OUT, targetClass.getStringValue())) - // SKIP CREATING EDGE - return null; + .isVertexConnectedTo((VertexInternal) currentFrom, currentTo, Vertex.DIRECTION.OUT, targetClass.getStringValue())) { + + for (Edge existingEdge : context.getDatabase().getGraphEngine() + .getEdges((VertexInternal) currentFrom, Vertex.DIRECTION.OUT, targetClass.getStringValue())) { + if (existingEdge.getOut().equals(currentTo)) { + currentTo = null; + currentBatch++; + return new UpdatableResult(existingEdge.modify()); + } + } + } + } final String target = targetBucket != null ? "bucket:" + targetBucket.getStringValue() : targetClass.getStringValue(); @@ -119,7 +138,7 @@ public Result next(final Object[] properties) { currentBatch++; return result; } finally { - if( context.isProfiling() ) { + if (context.isProfiling()) { cost += (System.nanoTime() - begin); } } @@ -223,7 +242,7 @@ protected void loadNextFromTo() { this.currentTo = null; } } finally { - if( context.isProfiling() ) { + if (context.isProfiling()) { cost += (System.nanoTime() - begin); } } @@ -271,7 +290,7 @@ public String prettyPrint(final int depth, final int indent) { result += spaces + " FOR EACH y in " + toAlias + "\n"; result += spaces + " CREATE EDGE " + targetClass + " FROM x TO y " + (unidirectional ? "UNIDIRECTIONAL" : "BIDIRECTIONAL"); - if ( context.isProfiling() ) + if (context.isProfiling()) result += " (" + getCostFormatted() + ")"; if (targetBucket != null) @@ -287,8 +306,14 @@ public boolean canBeCached() { @Override public ExecutionStep copy(final CommandContext context) { - return new CreateEdgesStep(targetClass == null ? null : targetClass.copy(), targetBucket == null ? null : targetBucket.copy(), - uniqueIndexName, fromAlias == null ? null : fromAlias.copy(), toAlias == null ? null : toAlias.copy(), unidirectional, - ifNotExists, context); + return new CreateEdgesStep( + targetClass == null ? null : targetClass.copy(), + targetBucket == null ? null : targetBucket.copy(), + uniqueIndexName, + fromAlias == null ? null : fromAlias.copy(), + toAlias == null ? null : toAlias.copy(), + unidirectional, + ifNotExists, + context); } } diff --git a/engine/src/test/java/com/arcadedb/query/sql/executor/CreateEdgeStatementExecutionTest.java b/engine/src/test/java/com/arcadedb/query/sql/executor/CreateEdgeStatementExecutionTest.java index 70ba2975e8..8f4003cff7 100644 --- a/engine/src/test/java/com/arcadedb/query/sql/executor/CreateEdgeStatementExecutionTest.java +++ b/engine/src/test/java/com/arcadedb/query/sql/executor/CreateEdgeStatementExecutionTest.java @@ -22,6 +22,7 @@ import com.arcadedb.exception.CommandSQLParsingException; import com.arcadedb.graph.Edge; import com.arcadedb.graph.MutableVertex; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -96,4 +97,52 @@ public void errorEdgesContentJsonArray() { // EXPECTED } } + + @Test + @DisplayName("createEdgeIfNotExists - test Issue #1763") + void createEdgeIfNotExists() { + database.transaction(() -> { + database.command("sqlscript", """ + CREATE VERTEX TYPE vex; + CREATE EDGE TYPE edg; + CREATE PROPERTY edg.label STRING; + CREATE VERTEX vex; + CREATE VERTEX vex; + CREATE VERTEX vex; + """); + }); + + database.transaction(() -> { + final ResultSet rs = database.query("SQL", """ + select from vex + """); + assertThat(rs.stream().count()).isEqualTo(3); + }); + // CREATE EDGES FROM #1:0 TO [#1:1,#1:2] + database.transaction(() -> { + final ResultSet rs = database.command("sql", """ + CREATE EDGE edg FROM #1:0 TO [#1:1,#1:2] IF NOT EXISTS + """); + assertThat(rs.stream().count()).isEqualTo(2); + }); + + // CREATE AGAIN (should not create any edge) + database.transaction(() -> { + final ResultSet rs = database.command("sql", """ + CREATE EDGE edg FROM #1:0 TO [#1:1,#1:2] IF NOT EXISTS + """); + assertThat(rs.hasNext()).isTrue(); + assertThat(rs.stream().count()).isEqualTo(2); + }); + + // CREATE AGAIN (should create 1 edge) + database.transaction(() -> { + final ResultSet rs = database.command("sql", """ + CREATE EDGE edg FROM #1:0 TO [#1:1,#1:2,#1:0] IF NOT EXISTS + """); + assertThat(rs.hasNext()).isTrue(); + assertThat(rs.stream().count()).isEqualTo(3); + }); + + } }