diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java index 5150430975..52656fd2ab 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java @@ -102,7 +102,6 @@ import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; import jakarta.ws.rs.ForbiddenException; @@ -308,8 +307,8 @@ protected final boolean removingEdgeOwner(HugeEdge edge) { return false; } + @Watched(prefix = "tx") @Override - @Watched(prefix = "graph") protected BackendMutation prepareCommit() { // Serialize and add updates into super.deletions if (!this.removedVertices.isEmpty() || !this.removedEdges.isEmpty()) { @@ -515,7 +514,6 @@ private void commitPartOfEdgeDeletions(Map removedEdges) { } @Override - @Watched(prefix = "graph") public void commit() throws BackendException { try { super.commit(); @@ -525,7 +523,6 @@ public void commit() throws BackendException { } @Override - @Watched(prefix = "graph") public void rollback() throws BackendException { // Rollback properties changes for (HugeProperty prop : this.updatedOldestProps) { @@ -539,7 +536,6 @@ public void rollback() throws BackendException { } @Override - @Watched(prefix = "graph") public QueryResults query(Query query) { if (!(query instanceof ConditionQuery)) { // It's a sysprop-query, don't need to optimize @@ -554,7 +550,6 @@ public QueryResults query(Query query) { } @Override - @Watched(prefix = "graph") public Number queryNumber(Query query) { boolean isConditionQuery = query instanceof ConditionQuery; boolean hasUpdate = this.hasUpdate(); @@ -714,7 +709,6 @@ public void removeVertex(HugeVertex vertex) { this.afterWrite(); } - @Watched(prefix = "graph") public Iterator queryAdjacentVertices(Iterator edges) { if (this.lazyLoadAdjacentVertex) { return new MapperIterator<>(edges, edge -> { @@ -732,18 +726,15 @@ public Iterator queryAdjacentVertices(Iterator edges) { }); } - @Watched(prefix = "graph") public Iterator queryAdjacentVertices(Object... vertexIds) { return this.queryVerticesByIds(vertexIds, true, this.checkAdjacentVertexExist); } - @Watched(prefix = "graph") public Iterator queryVertices(Object... vertexIds) { return this.queryVerticesByIds(vertexIds, false, false); } - @Watched(prefix = "graph") public Vertex queryVertex(Object vertexId) { Iterator iter = this.queryVerticesByIds(new Object[]{vertexId}, false, true); @@ -784,131 +775,117 @@ protected Iterator queryVerticesByIds(Object[] vertexIds, boolean adjace return this.queryVerticesByIds(vertexIds, adjacentVertex, checkMustExist, HugeType.VERTEX); } - @Watched(prefix = "graph") protected Iterator queryVerticesByIds(Object[] vertexIds, boolean adjacentVertex, boolean checkMustExist, HugeType type) { Query.checkForceCapacity(vertexIds.length); - List ids; - Map vertices; - boolean verticesUpdated = this.verticesInTxSize() > 0; - if (vertexIds.length == 1) { - Id id = HugeVertex.getIdValue(vertexIds[0]); + // Fast path: skip the id list, map and mapper iterator for one id + return this.queryVertexById(vertexIds[0], adjacentVertex, + checkMustExist, type); + } - boolean tryQueryBackend = true; - if (id == null) { - tryQueryBackend = false; - ids = ImmutableList.of(); - } else { - ids = ImmutableList.of(id); - } - - HugeVertex vertex = null; - if (id != null && verticesUpdated) { - if (this.removedVertices.containsKey(id)) { - // The record has been deleted - tryQueryBackend = false; - } else if ((vertex = this.addedVertices.get(id)) != null || - (vertex = this.updatedVertices.get(id)) != null) { - // Found from local tx - tryQueryBackend = false; - if (vertex.expired()) { - vertex = null; - } else { - assert vertex != null; - } - } - } + // NOTE: allowed duplicated vertices if query by duplicated ids + List ids = InsertionOrderUtil.newList(); + Map vertices = new HashMap<>(vertexIds.length); - if (vertex != null) { - assert !tryQueryBackend; - vertices = ImmutableMap.of(vertex.id(), vertex); - } else if (!tryQueryBackend) { - assert vertex == null; - vertices = ImmutableMap.of(); - } else { - // Query from backend store - IdQuery query = new IdQuery.OneIdQuery(type, id); - Iterator it = this.queryVerticesFromBackend(query); - vertex = QueryResults.one(it); - if (vertex == null) { - vertices = ImmutableMap.of(); - } else { - vertices = ImmutableMap.of(vertex.id(), vertex); - } - } - } else { - // NOTE: allowed duplicated vertices if query by duplicated ids - ids = InsertionOrderUtil.newList(); - vertices = new HashMap<>(vertexIds.length); - - IdQuery query = new IdQuery(type); - for (Object vertexId : vertexIds) { - Id id = HugeVertex.getIdValue(vertexId); - if (id == null) { + IdQuery query = new IdQuery(type); + for (Object vertexId : vertexIds) { + HugeVertex vertex; + Id id = HugeVertex.getIdValue(vertexId); + if (id == null || this.removedVertices.containsKey(id)) { + // The record has been deleted + continue; + } else if ((vertex = this.addedVertices.get(id)) != null || + (vertex = this.updatedVertices.get(id)) != null) { + if (vertex.expired()) { continue; } - boolean foundLocal = false; - if (verticesUpdated) { - HugeVertex vertex; - if (this.removedVertices.containsKey(id)) { - // The record has been deleted - continue; - } - if ((vertex = this.addedVertices.get(id)) != null || - (vertex = this.updatedVertices.get(id)) != null) { - if (vertex.expired()) { - continue; - } - // Found from local tx - foundLocal = true; - vertices.put(vertex.id(), vertex); - } else { - assert !foundLocal; - } - } - if (!foundLocal) { - // Prepare to query from backend store - query.query(id); - } - ids.add(id); + // Found from local tx + vertices.put(vertex.id(), vertex); + } else { + // Prepare to query from backend store + query.query(id); } + ids.add(id); + } - if (!query.empty()) { - // Query from backend store - query.mustSortByInput(false); - Iterator it = this.queryVerticesFromBackend(query); - QueryResults.fillMap(it, vertices); - } + if (!query.empty()) { + // Query from backend store + query.mustSortByInput(false); + Iterator it = this.queryVerticesFromBackend(query); + QueryResults.fillMap(it, vertices); } return new MapperIterator<>(ids.iterator(), id -> { - HugeVertex vertex = vertices.get(id); + return this.resolveVertex(vertices.get(id), id, + adjacentVertex, checkMustExist); + }); + } + + /** + * Query a single vertex by id, with the same semantics as the multi-id + * path of {@link #queryVerticesByIds(Object[], boolean, boolean, HugeType)} + * but without allocating the id list, the result map and the mapper + * iterator; only an {@link IdQuery.OneIdQuery} is created on a miss. + */ + private Iterator queryVertexById(Object vertexId, boolean adjacentVertex, + boolean checkMustExist, HugeType type) { + Id id = HugeVertex.getIdValue(vertexId); + if (id == null) { + return QueryResults.emptyIterator(); + } + + HugeVertex vertex = null; + if (this.verticesInTxSize() > 0) { + if (this.removedVertices.containsKey(id)) { + // The record has been deleted + return QueryResults.emptyIterator(); + } + vertex = this.addedVertices.get(id); if (vertex == null) { - if (checkMustExist) { - throw new NotFoundException( - "Vertex '%s' does not exist", id); - } else if (adjacentVertex) { - assert !checkMustExist; - // Return undefined if adjacentVertex but !checkMustExist - vertex = HugeVertex.undefined(this.graph(), id); - } else { - // Return null - assert vertex == null; - } + vertex = this.updatedVertices.get(id); + } + if (vertex != null && vertex.expired()) { + // Found from local tx but expired + return QueryResults.emptyIterator(); } + } + + if (vertex == null) { + // Query from backend store + IdQuery query = new IdQuery.OneIdQuery(type, id); + vertex = QueryResults.one(this.queryVerticesFromBackend(query)); + } + + vertex = this.resolveVertex(vertex, id, adjacentVertex, checkMustExist); + if (vertex == null) { + return QueryResults.emptyIterator(); + } + return QueryResults.iterator(vertex); + } + + private HugeVertex resolveVertex(HugeVertex vertex, Id id, + boolean adjacentVertex, boolean checkMustExist) { + if (vertex != null) { return vertex; - }); + } + if (checkMustExist) { + throw new NotFoundException("Vertex '%s' does not exist", id); + } + if (adjacentVertex) { + // Return undefined if adjacentVertex but !checkMustExist + return HugeVertex.undefined(this.graph(), id); + } + // Return null to skip the vertex + return null; } - @Watched(prefix = "graph") public Iterator queryVertices() { Query q = new Query(HugeType.VERTEX); return this.queryVertices(q); } - @Watched(prefix = "graph") public Iterator queryVertices(Query query) { if (this.hasUpdate()) { E.checkArgument(query.noLimitAndOffset(), @@ -930,7 +907,6 @@ public Iterator queryVertices(Query query) { return this.skipOffsetOrStopLimit(r, query); } - @Watched(prefix = "graph") protected Iterator queryVerticesFromBackend(Query query) { assert query.resultType().isVertex(); @@ -992,17 +968,14 @@ public void removeEdge(HugeEdge edge) { this.afterWrite(); } - @Watched(prefix = "graph") public Iterator queryEdgesByVertex(Id id) { return this.queryEdges(constructEdgesQuery(id, Directions.BOTH, new Id[0])); } - @Watched(prefix = "graph") public Iterator queryEdges(Object... edgeIds) { return this.queryEdgesByIds(edgeIds, false); } - @Watched(prefix = "graph") public Edge queryEdge(Object edgeId) { Iterator iter = this.queryEdgesByIds(new Object[]{edgeId}, true); Edge edge = QueryResults.one(iter); @@ -1012,121 +985,62 @@ public Edge queryEdge(Object edgeId) { return edge; } - @Watched(prefix = "graph") protected Iterator queryEdgesByIds(Object[] edgeIds, boolean verifyId) { Query.checkForceCapacity(edgeIds.length); - List ids; - Map edges; - boolean edgesUpdated = this.edgesInTxSize() > 0; - if (edgeIds.length == 1) { - EdgeId id = HugeEdge.getIdValue(edgeIds[0], !verifyId); + // Fast path: skip the id list, map and mapper iterator for one id + return this.queryEdgeById(edgeIds[0], verifyId); + } + + // NOTE: allowed duplicated edges if query by duplicated ids + List ids = InsertionOrderUtil.newList(); + Map edges = new HashMap<>(edgeIds.length); - boolean tryQueryBackend = true; + IdQuery query = new IdQuery(HugeType.EDGE); + for (Object edgeId : edgeIds) { + HugeEdge edge; + EdgeId id = HugeEdge.getIdValue(edgeId, !verifyId); if (id == null) { - tryQueryBackend = false; - ids = ImmutableList.of(); - } else { - if (id.direction() == Directions.IN) { - id = id.switchDirection(); - } - ids = ImmutableList.of(id); - } - - HugeEdge edge = null; - if (id != null && edgesUpdated) { - if (this.removedEdges.containsKey(id)) { - // The record has been deleted - tryQueryBackend = false; - } else if ((edge = this.addedEdges.get(id)) != null || - (edge = this.updatedEdges.get(id)) != null) { - // Found from local tx - tryQueryBackend = false; - if (edge.expired()) { - edge = null; - } else { - assert edge != null; - } - } + continue; } - - if (edge != null) { - assert !tryQueryBackend; - edges = ImmutableMap.of(edge.id(), edge); - } else if (!tryQueryBackend) { - assert edge == null; - edges = ImmutableMap.of(); - } else { - // Query from backend store - IdQuery query = new IdQuery.OneIdQuery(HugeType.EDGE, id); - Iterator it = this.queryEdgesFromBackend(query); - edge = QueryResults.one(it); - if (edge == null) { - edges = ImmutableMap.of(); - } else { - edges = ImmutableMap.of(edge.id(), edge); - } + if (id.direction() == Directions.IN) { + id = id.switchDirection(); } - } else { - // NOTE: allowed duplicated edges if query by duplicated ids - ids = InsertionOrderUtil.newList(); - edges = new HashMap<>(edgeIds.length); - - IdQuery query = new IdQuery(HugeType.EDGE); - for (Object edgeId : edgeIds) { - HugeEdge edge; - EdgeId id = HugeEdge.getIdValue(edgeId, !verifyId); - if (id == null) { + if (this.removedEdges.containsKey(id)) { + // The record has been deleted + continue; + } else if ((edge = this.addedEdges.get(id)) != null || + (edge = this.updatedEdges.get(id)) != null) { + if (edge.expired()) { continue; } - if (id.direction() == Directions.IN) { - id = id.switchDirection(); - } - - boolean foundLocal = false; - if (edgesUpdated) { - if (this.removedEdges.containsKey(id)) { - // The record has been deleted - continue; - } - if ((edge = this.addedEdges.get(id)) != null || - (edge = this.updatedEdges.get(id)) != null) { - if (edge.expired()) { - continue; - } - // Found from local tx - foundLocal = true; - edges.put(edge.id(), edge); - } else { - assert !foundLocal; - } - } - if (!foundLocal) { - // Prepare to query from backend store - query.query(id); - } - ids.add(id); + // Found from local tx + edges.put(edge.id(), edge); + } else { + // Prepare to query from backend store + query.query(id); } + ids.add(id); + } - if (!query.empty()) { - // Query from backend store - if (edges.isEmpty() && query.idsSize() == ids.size()) { - /* - * Sort at the lower layer and return directly if there is - * no local vertex and duplicated id. - */ - Iterator it = this.queryEdgesFromBackend(query); - @SuppressWarnings({ "unchecked", "rawtypes" }) - Iterator r = (Iterator) it; - return r; - } - - query.mustSortByInput(false); + if (!query.empty()) { + // Query from backend store + if (edges.isEmpty() && query.idsSize() == ids.size()) { + /* + * Sort at the lower layer and return directly if there is no + * local vertex and duplicated id. + */ Iterator it = this.queryEdgesFromBackend(query); - QueryResults.fillMap(it, edges); + @SuppressWarnings({"unchecked", "rawtypes"}) + Iterator r = (Iterator) it; + return r; } + + query.mustSortByInput(false); + Iterator it = this.queryEdgesFromBackend(query); + QueryResults.fillMap(it, edges); } return new MapperIterator<>(ids.iterator(), id -> { @@ -1135,7 +1049,50 @@ protected Iterator queryEdgesByIds(Object[] edgeIds, }); } - @Watched(prefix = "graph") + /** + * Query a single edge by id, with the same semantics as the multi-id + * path of {@link #queryEdgesByIds(Object[], boolean)} but without + * allocating the id list, the result map and the mapper iterator; only + * an {@link IdQuery.OneIdQuery} is created on a miss. + */ + private Iterator queryEdgeById(Object edgeId, boolean verifyId) { + EdgeId id = HugeEdge.getIdValue(edgeId, !verifyId); + if (id == null) { + return QueryResults.emptyIterator(); + } + if (id.direction() == Directions.IN) { + id = id.switchDirection(); + } + + if (this.edgesInTxSize() > 0) { + if (this.removedEdges.containsKey(id)) { + // The record has been deleted + return QueryResults.emptyIterator(); + } + HugeEdge edge = this.addedEdges.get(id); + if (edge == null) { + edge = this.updatedEdges.get(id); + } + if (edge != null) { + // Found from local tx + if (edge.expired()) { + return QueryResults.emptyIterator(); + } + return QueryResults.iterator(edge); + } + } + + /* + * Query from backend store and return the results directly, just + * like the multi-id path does when there is no local edge. + */ + IdQuery query = new IdQuery.OneIdQuery(HugeType.EDGE, id); + Iterator it = this.queryEdgesFromBackend(query); + @SuppressWarnings({"unchecked", "rawtypes"}) + Iterator r = (Iterator) it; + return r; + } + public Iterator queryEdges() { Query q = new Query(HugeType.EDGE); return this.queryEdges(q); @@ -1189,7 +1146,6 @@ public Iterator queryEdges(Query query) { return this.skipOffsetOrStopLimit(r, query); } - @Watched(prefix = "graph") protected Iterator queryEdgesFromBackend(Query query) { assert query.resultType().isEdge(); @@ -1687,7 +1643,7 @@ private Query optimizeQuery(ConditionQuery query) { * Just query by primary-key(id), ignore other user-props(if exists) * that it will be filtered by queryVertices(Query) */ - return new IdQuery.OneIdQuery(query, id); + return new IdQuery(query, id); } } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java index 265d408742..12a8a66e2b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; @@ -33,6 +34,7 @@ import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.backend.id.EdgeId; import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.page.PageInfo; @@ -46,6 +48,7 @@ import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; +import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.schema.Userdata; import org.apache.hugegraph.structure.HugeEdge; @@ -2728,6 +2731,176 @@ public void testQueryEdgesByIdWithGraphAPIAndNotCommittedRemoved() { Assert.assertTrue(graph.edges(edge1.id(), edge2.id()).hasNext()); } + @Test + public void testQueryEdgesByIdsWithLocalAndDuplicateIds() { + HugeGraph graph = graph(); + init18Edges(); + + Object id1 = graph.traversal().E().toList().get(0).id(); + // Added but not committed + Vertex james = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Vertex book = graph.addVertex(T.label, "book", "name", "c-book"); + Edge local = james.addEdge("authored", book, "score", 5); + Object id2 = local.id(); + + List edges = ImmutableList.copyOf(graph.edges(id2, id1, id2)); + Assert.assertEquals(3, edges.size()); + Assert.assertSame(local, edges.get(0)); + Assert.assertEquals(id1, edges.get(1).id()); + Assert.assertSame(local, edges.get(2)); + + graph.tx().rollback(); + } + + @Test + public void testQuerySingleEdgeByIdInLocalTx() { + HugeGraph graph = graph(); + init18Edges(); + + // Added but not committed + Vertex james = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Vertex book = graph.addVertex(T.label, "book", "name", "c-book"); + Edge edge = james.addEdge("authored", book, "score", 5); + Object id = edge.id(); + + List edges = ImmutableList.copyOf(graph.edges(id)); + Assert.assertEquals(1, edges.size()); + Assert.assertSame(edge, edges.get(0)); + Assert.assertSame(edge, graph.edge(id)); + graph.tx().commit(); + + // Updated but not committed + edge = graph.edge(id); + edge.property("score", 6); + + edges = ImmutableList.copyOf(graph.edges(id)); + Assert.assertEquals(1, edges.size()); + Assert.assertSame(edge, edges.get(0)); + Assert.assertEquals(6, (int) graph.edge(id).value("score")); + graph.tx().rollback(); + + Assert.assertEquals(5, (int) graph.edge(id).value("score")); + } + + @Test + public void testQuerySingleEdgeByIdRemovedInLocalTx() { + HugeGraph graph = graph(); + init18Edges(); + + Edge edge = graph.traversal().E().toList().get(0); + Object id = edge.id(); + Assert.assertTrue(graph.edges(id).hasNext()); + + edge.remove(); + Assert.assertFalse(graph.edges(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + graph.tx().rollback(); + Assert.assertTrue(graph.edges(id).hasNext()); + Assert.assertEquals(id, graph.edge(id).id()); + } + + @Test + public void testQuerySingleEdgeByIdNotFound() { + HugeGraph graph = graph(); + init18Edges(); + + String id = graph.traversal().E().toList().get(0).id() + "-not-exist"; + Assert.assertFalse(graph.edges(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + } + + @Test + public void testQuerySingleEdgeByInvalidId() { + HugeGraph graph = graph(); + init18Edges(); + + // Invalid id is skipped by edges() and rejected by edge() + Assert.assertFalse(graph.edges("invalid-edge-id").hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge("invalid-edge-id"); + }, e -> { + Assert.assertContains("Edge id must be formatted", e.getMessage()); + }); + + Assert.assertFalse(graph.edges((Object) null).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(null); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + } + + @Test + public void testQuerySingleEdgeByIdWithInDirection() { + HugeGraph graph = graph(); + init18Edges(); + + HugeEdge edge = (HugeEdge) graph.traversal().E().toList().get(0); + EdgeId inId = edge.idWithDirection().switchDirection(); + Assert.assertEquals(Directions.IN, inId.direction()); + + List edges = ImmutableList.copyOf(graph.edges(inId)); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals(edge.id(), edges.get(0).id()); + Assert.assertEquals(edge.id(), graph.edge(inId).id()); + + edges = ImmutableList.copyOf(graph.edges(inId.asString())); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals(edge.id(), edges.get(0).id()); + } + + @Test + public void testQuerySingleEdgeByIdExpiredInLocalTx() { + HugeGraph graph = graph(); + + Vertex baby = graph.addVertex(T.label, "person", "name", "Baby", + "age", 3, "city", "Beijing"); + Vertex java = graph.addVertex(T.label, "book", + "name", "Java in action"); + Edge edge = baby.addEdge("read", java, "place", "library of school", + "date", "2019-12-23 12:00:00"); + Object id = edge.id(); + graph.tx().commit(); + + edge = graph.edge(id); + Assert.assertTrue(graph.edges(id).hasNext()); + graph.tx().rollback(); + + try { + Thread.sleep(3100L); + } catch (InterruptedException e) { + // Ignore + } + + // Update the expired edge in a new tx (not committed) + edge.property("place", "home"); + Map updated = Whitebox.getInternalState( + params().graphTransaction(), "updatedEdges"); + Assert.assertTrue(updated.containsKey(edge.id())); + Assert.assertTrue(((HugeEdge) edge).expired()); + + Assert.assertFalse(graph.edges(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + graph.tx().rollback(); + } + @Test public void testQueryEdgesByIdNotFound() { HugeGraph graph = graph(); @@ -3326,6 +3499,49 @@ public void testQueryAdjacentVerticesOfEdgesWithoutVertex() } } + @Test + public void testQueryAdjacentVertexRemovedInLocalTx() + throws InterruptedException, ExecutionException { + HugeGraph graph = graph(); + + Vertex james = graph.addVertex(T.label, "author", "id", 1, + "name", "James Gosling", "age", 62, + "lived", "Canadian"); + Vertex java = graph.addVertex(T.label, "book", "name", "java"); + james.addEdge("authored", java, "score", 3); + graph.tx().commit(); + params().graphEventHub().notify(Events.CACHE, "clear", null).get(); + + Edge edge = graph.traversal().V(james.id()).outE().next(); + HugeVertex adjacent = (HugeVertex) edge.inVertex(); + Assert.assertFalse(adjacent.isPropLoaded()); + Assert.assertEquals("book", adjacent.label()); + + // Remove the adjacent vertex but don't commit + graph.vertex(java.id()).remove(); + Assert.assertFalse(graph.vertices(java.id()).hasNext()); + Assert.assertFalse(graph.adjacentVertex(java.id()).hasNext()); + // Querying by one id must agree with querying by multiple ids + List vertices = ImmutableList.copyOf( + params().graphTransaction() + .queryAdjacentVertices(java.id(), james.id())); + Assert.assertEquals(1, vertices.size()); + Assert.assertEquals(james.id(), vertices.get(0).id()); + + /* + * Loading the adjacent vertex of an edge held before the removal + * must not turn it into an undefined vertex + */ + adjacent.forceLoad(); + Assert.assertFalse(adjacent.schemaLabel().undefined()); + Assert.assertEquals("book", adjacent.label()); + + graph.tx().rollback(); + Assert.assertTrue(graph.vertices(java.id()).hasNext()); + Assert.assertEquals("book", graph.adjacentVertex(java.id()) + .next().label()); + } + @Test public void testQueryAdjacentVerticesOfEdgesWithInvalidVertexLabel() throws InterruptedException, ExecutionException { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index a329de3afb..861f295975 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; @@ -51,11 +52,13 @@ import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; +import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.schema.Userdata; import org.apache.hugegraph.schema.VertexLabel; import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.FakeObjects; import org.apache.hugegraph.testutil.Utils; @@ -3124,6 +3127,174 @@ public void testQueryByIdWithGraphAPIAndNotCommittedRemoved() { Assert.assertTrue(graph.vertices(vertex1.id(), vertex2.id()).hasNext()); } + @Test + public void testQueryVerticesByNonConsecutiveDuplicateIds() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + List all = graph.traversal().V().toList(); + Object id1 = all.get(0).id(); + Object id2 = all.get(1).id(); + + List vertices = ImmutableList.copyOf(graph.vertices(id1, id2, id1)); + Assert.assertEquals(3, vertices.size()); + Assert.assertEquals(id1, vertices.get(0).id()); + Assert.assertEquals(id2, vertices.get(1).id()); + Assert.assertEquals(id1, vertices.get(2).id()); + } + + @Test + public void testQueryVerticesByIdsWithLocalAndDuplicateIds() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Object id1 = graph.traversal().V().toList().get(0).id(); + // Added but not committed + Vertex local = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Object id2 = local.id(); + + List vertices = ImmutableList.copyOf(graph.vertices(id2, id1, id2)); + Assert.assertEquals(3, vertices.size()); + Assert.assertSame(local, vertices.get(0)); + Assert.assertEquals(id1, vertices.get(1).id()); + Assert.assertSame(local, vertices.get(2)); + + graph.tx().rollback(); + } + + @Test + public void testQuerySingleVertexByIdInLocalTx() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + // Added but not committed + Vertex vertex = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Object id = vertex.id(); + + List vertices = ImmutableList.copyOf(graph.vertices(id)); + Assert.assertEquals(1, vertices.size()); + Assert.assertSame(vertex, vertices.get(0)); + Assert.assertSame(vertex, graph.vertex(id)); + Assert.assertSame(vertex, graph.adjacentVertex(id).next()); + this.commitTx(); + + // Updated but not committed + vertex = graph.vertex(id); + vertex.property("age", 71); + + vertices = ImmutableList.copyOf(graph.vertices(id)); + Assert.assertEquals(1, vertices.size()); + Assert.assertSame(vertex, vertices.get(0)); + Assert.assertEquals(71, (int) graph.vertex(id).value("age")); + graph.tx().rollback(); + + Assert.assertEquals(70, (int) graph.vertex(id).value("age")); + } + + @Test + public void testQuerySingleVertexByIdRemovedInLocalTx() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Vertex vertex = graph.traversal().V().hasLabel("author") + .has("id", 1).next(); + Object id = vertex.id(); + Assert.assertTrue(graph.vertices(id).hasNext()); + Assert.assertTrue(graph.adjacentVertex(id).hasNext()); + + vertex.remove(); + Assert.assertFalse(graph.vertices(id).hasNext()); + // Removed vertex must not be reported as an undefined adjacent vertex + Assert.assertFalse(graph.adjacentVertex(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + graph.tx().rollback(); + Assert.assertTrue(graph.vertices(id).hasNext()); + Assert.assertTrue(graph.adjacentVertex(id).hasNext()); + } + + @Test + public void testQuerySingleVertexByIdNotFound() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Id id = SplicingIdGenerator.splicing("author", "not-exists-id"); + Assert.assertFalse(graph.vertices(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + // Adjacent vertex not found is returned as an undefined vertex + // (vertex.check_adjacent_vertex_exist=false, the default) + List vertices = ImmutableList.copyOf(graph.adjacentVertex(id)); + Assert.assertEquals(1, vertices.size()); + Assert.assertEquals(id, vertices.get(0).id()); + Assert.assertEquals("~undefined", vertices.get(0).label()); + } + + @Test + public void testQuerySingleVertexByNullId() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Assert.assertFalse(graph.vertices((Object) null).hasNext()); + Assert.assertFalse(graph.adjacentVertex(null).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(null); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + // Same as querying by multiple ids + Assert.assertFalse(graph.vertices(null, null).hasNext()); + } + + @Test + public void testQuerySingleVertexByIdExpiredInLocalTx() { + HugeGraph graph = graph(); + + Vertex vertex = graph.addVertex(T.label, "fan", "name", "Baby", + "age", 3, "city", "Beijing"); + Object id = vertex.id(); + this.commitTx(); + + vertex = graph.vertex(id); + Assert.assertTrue(graph.vertices(id).hasNext()); + graph.tx().rollback(); + + try { + Thread.sleep(3100L); + } catch (InterruptedException e) { + // Ignore + } + + // Update the expired vertex in a new tx (not committed) + vertex.property("age", 4); + Map updated = Whitebox.getInternalState( + params().graphTransaction(), "updatedVertices"); + Assert.assertTrue(updated.containsKey(vertex.id())); + Assert.assertTrue(((HugeVertex) vertex).expired()); + + Assert.assertFalse(graph.vertices(id).hasNext()); + Assert.assertFalse(graph.adjacentVertex(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + graph.tx().rollback(); + } + @Test public void testQueryByInvalidSysprop() { HugeGraph graph = graph();