You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/08 15:34:58 UTC

(hugegraph) 08/15: fix(server): preserve schema and index semantics

This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git

commit 736a99056f14ceae371066e631bd0352a9163478
Author: imbajin <ji...@apache.org>
AuthorDate: Thu Oct 8 20:31:40 2026 +0800

    fix(server): preserve schema and index semantics
    
    - retain nullable and label semantics across cleanup
    - refresh graph index schema identities consistently
    - cover vertex, edge and index operations
---
 .../backend/tx/GraphIndexTransaction.java          | 41 +++++++++---
 .../backend/tx/GraphIndexTransactionTest.java      |  5 ++
 .../org/apache/hugegraph/core/EdgeCoreTest.java    | 42 +++++++++++++
 .../apache/hugegraph/core/IndexLabelCoreTest.java  | 73 ++++++++++++++++++++++
 .../org/apache/hugegraph/core/VertexCoreTest.java  | 19 ++++++
 5 files changed, 173 insertions(+), 7 deletions(-)

diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java
index b333c929d..5b5289dd5 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java
@@ -148,14 +148,24 @@ public class GraphIndexTransaction extends AbstractTransaction {
             return;
         }
         // Update index(only property, no edge) of a vertex
-        IndexBuilder.forEachIndexLabelId(vertex.element(), this.graph()::edgeLabel,
-                                         id -> this.updateIndex(id, vertex, removed));
+        for (Id id : this.indexLabelIds(vertex.schemaLabel())) {
+            this.updateIndex(id, vertex, removed);
+        }
     }
 
     @Watched(prefix = "index")
     public void updateEdgeIndex(HugeEdge edge, boolean removed) {
-        IndexBuilder.forEachIndexLabelId(edge.element(), this.graph()::edgeLabel,
-                                         id -> this.updateIndex(id, edge, removed));
+        // Update index of an edge
+        for (Id id : this.indexLabelIds(edge.schemaLabel())) {
+            this.updateIndex(id, edge, removed);
+        }
+
+        EdgeLabel label = edge.schemaLabel();
+        if (label.hasFather()) {
+            for (Id id : graph().edgeLabel(label.fatherId()).indexLabels()) {
+                this.updateIndex(id, edge, removed);
+            }
+        }
     }
 
     private void updateVertexOlapIndex(HugeVertex vertex, boolean removed) {
@@ -177,6 +187,9 @@ public class GraphIndexTransaction extends AbstractTransaction {
         E.checkArgument(indexLabel != null,
                         "Not exist index label with id '%s'", ilId);
 
+        if (!(element instanceof HugeVertex && ((HugeVertex) element).olap())) {
+            element.element().schemaLabel(this.currentSchemaLabel(element.schemaLabel()));
+        }
         this.indexBuilder.forEachIndex(element.element(), indexLabel, index -> {
             Object value = index.fieldValues();
             if (indexLabel.indexType().isUnique() && !removed &&
@@ -1386,9 +1399,23 @@ public class GraphIndexTransaction extends AbstractTransaction {
                         indexLabel, indexLabel.status());
     }
 
-    private static Set<IndexLabel> relatedIndexLabels(HugeElement element) {
+    Set<Id> indexLabelIds(SchemaLabel label) {
+        return this.currentSchemaLabel(label).indexLabels();
+    }
+
+    private SchemaLabel currentSchemaLabel(SchemaLabel label) {
+        // Elements can outlive schema cache eviction. Resolve the current index
+        // membership rather than using the schema object retained by an element.
+        ISchemaTransaction schema = this.params().schemaTransaction();
+        SchemaLabel current = label.type() == HugeType.VERTEX_LABEL ?
+                              schema.getVertexLabel(label.id()) : schema.getEdgeLabel(label.id());
+        // Missing schema is represented by an undefined label so orphan records remain removable.
+        return current != null ? current : label;
+    }
+
+    private Set<IndexLabel> relatedIndexLabels(HugeElement element) {
         Set<IndexLabel> indexLabels = InsertionOrderUtil.newSet();
-        Set<Id> indexLabelIds = element.schemaLabel().indexLabels();
+        Set<Id> indexLabelIds = this.indexLabelIds(element.schemaLabel());
 
         for (Id id : indexLabelIds) {
             IndexLabel indexLabel = element.graph().indexLabel(id);
@@ -1643,7 +1670,7 @@ public class GraphIndexTransaction extends AbstractTransaction {
             // Delete unused index
             long count = 0;
             Set<Id> incorrectPkIds;
-            for (IndexLabel il : relatedIndexLabels(deletion)) {
+            for (IndexLabel il : this.tx.relatedIndexLabels(deletion)) {
                 incorrectPkIds = incorrectPKs.keySet().stream()
                                              .map(PropertyKey::id)
                                              .collect(Collectors.toSet());
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java
index ed945554a..62325f0ae 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java
@@ -29,6 +29,7 @@ import org.apache.hugegraph.analyzer.Analyzer;
 import org.apache.hugegraph.backend.serializer.AbstractSerializer;
 import org.apache.hugegraph.backend.store.BackendEntry;
 import org.apache.hugegraph.backend.store.BackendStore;
+import org.apache.hugegraph.id.Id;
 import org.apache.hugegraph.id.IdGenerator;
 import org.apache.hugegraph.id.EdgeId;
 import org.apache.hugegraph.query.ConditionQuery;
@@ -230,6 +231,10 @@ public class GraphIndexTransactionTest {
         Mockito.doReturn(objects.graph().configuration()).when(params).configuration();
         Mockito.when(params.serializer()).thenReturn(serializer);
         Mockito.when(params.schemaTransaction()).thenReturn(schema);
+        Mockito.when(schema.getVertexLabel(Mockito.any(Id.class)))
+               .thenAnswer(call -> objects.graph().vertexLabel((Id) call.getArgument(0)));
+        Mockito.when(schema.getEdgeLabel(Mockito.any(Id.class)))
+               .thenAnswer(call -> objects.graph().edgeLabel((Id) call.getArgument(0)));
         Mockito.when(schema.getIndexLabels()).thenReturn(labels);
         Mockito.when(params.analyzer()).thenReturn(analyzer);
         for (IndexLabel label : labels) {
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 d626df4a1..c69ef7254 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
@@ -213,6 +213,48 @@ public class EdgeCoreTest extends BaseCoreTest {
               .by("place", "tool", "reason").create();
     }
 
+    @Test
+    public void testDeleteEdgeWithMissingSchemaLabel() throws InterruptedException, ExecutionException {
+        HugeGraph graph = graph();
+        Vertex author = graph.addVertex(T.label, "author", "id", 1, "name", "James",
+                                        "age", 62, "lived", "Canadian");
+        Vertex book = graph.addVertex(T.label, "book", "name", "Java");
+        Edge edge = author.addEdge("authored", book, "score", 3);
+        graph.tx().commit();
+
+        // Preserve the backend edge while removing its schema metadata.
+        params().schemaTransaction().removeSchema(graph.edgeLabel("authored"));
+        params().graphEventHub().notify(Events.CACHE, "clear", null).get();
+        Edge orphan = graph.edges(edge.id()).next();
+        Assert.assertEquals("~undefined", orphan.label());
+
+        orphan.remove();
+        graph.tx().commit();
+        Assert.assertFalse(graph.edges(edge.id()).hasNext());
+        Assert.assertFalse(graph.vertices(author.id()).next().edges(Direction.OUT).hasNext());
+        Assert.assertFalse(graph.vertices(book.id()).next().edges(Direction.IN).hasNext());
+    }
+
+    @Test
+    public void testDeleteVertexAdjacentToEdgeWithMissingSchemaLabel() throws InterruptedException, ExecutionException {
+        HugeGraph graph = graph();
+        Vertex author = graph.addVertex(T.label, "author", "id", 1, "name", "James",
+                                        "age", 62, "lived", "Canadian");
+        Vertex book = graph.addVertex(T.label, "book", "name", "Java");
+        Edge edge = author.addEdge("authored", book, "score", 3);
+        graph.tx().commit();
+
+        params().schemaTransaction().removeSchema(graph.edgeLabel("authored"));
+        params().graphEventHub().notify(Events.CACHE, "clear", null).get();
+        Assert.assertEquals("~undefined", graph.edges(edge.id()).next().label());
+
+        graph.vertices(author.id()).next().remove();
+        graph.tx().commit();
+        Assert.assertFalse(graph.vertices(author.id()).hasNext());
+        Assert.assertFalse(graph.edges(edge.id()).hasNext());
+        Assert.assertFalse(graph.vertices(book.id()).next().edges(Direction.IN).hasNext());
+    }
+
     @Test
     public void testAddEdge() {
         HugeGraph graph = graph();
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java
index c9988db51..6a7fdb60a 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/IndexLabelCoreTest.java
@@ -22,6 +22,7 @@ import java.util.List;
 
 import org.apache.hugegraph.exception.HugeException;
 import org.apache.hugegraph.HugeGraph;
+import org.apache.hugegraph.HugeGraphParams;
 import org.apache.hugegraph.exception.ExistedException;
 import org.apache.hugegraph.exception.NoIndexException;
 import org.apache.hugegraph.exception.NotFoundException;
@@ -31,10 +32,12 @@ import org.apache.hugegraph.schema.SchemaManager;
 import org.apache.hugegraph.struct.schema.Userdata;
 import org.apache.hugegraph.struct.schema.VertexLabel;
 import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.testutil.Whitebox;
 import org.apache.hugegraph.type.HugeType;
 import org.apache.hugegraph.type.define.IndexType;
 import org.apache.hugegraph.type.define.WriteType;
 import org.apache.hugegraph.util.DateUtil;
+import org.apache.hugegraph.util.Events;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.T;
@@ -1297,6 +1300,76 @@ public class IndexLabelCoreTest extends SchemaCoreTest {
         });
     }
 
+    @Test
+    public void testUpdateCachedVertexAfterIndexRemoval() throws Exception {
+        super.initPropertyKeys();
+        SchemaManager schema = graph().schema();
+        schema.vertexLabel("person").properties("name", "city").primaryKeys("name").create();
+        schema.indexLabel("personByCity").onV("person").secondary().by("city").create();
+        Vertex original = graph().addVertex(T.label, "person", "name", "cache-test", "city", "old");
+        graph().tx().commit();
+        Vertex cached = graph().vertices(original.id()).next();
+        graph().tx().commit();
+        // Schema cache eviction must not make a retained vertex's index list authoritative.
+        HugeGraphParams params = Whitebox.getInternalState(graph(), "params");
+        params.schemaEventHub().notify(Events.CACHE, "clear", null).get();
+
+        schema.indexLabel("personByCity").remove();
+        Assert.assertThrows(NotFoundException.class, () -> schema.getIndexLabel("personByCity"));
+        cached.property("city", "new");
+        graph().tx().commit();
+        Assert.assertEquals("new", graph().vertices(original.id()).next().value("city"));
+        graph().tx().commit();
+        schema.indexLabel("personByCity").onV("person").secondary().by("city").create();
+        schema.propertyKey("extra").asText().create();
+        schema.vertexLabel("person").properties("extra").nullableKeys("extra").append();
+        schema.indexLabel("personByExtra").onV("person").secondary().by("extra").create();
+        graph().taskScheduler().waitUntilAllTasksCompleted(30);
+        cached.property("city", "newer");
+        graph().tx().commit();
+        Assert.assertFalse(graph().traversal().V().has("city", "new").hasNext());
+        Assert.assertEquals(original.id(), graph().traversal().V().has("city", "newer").next().id());
+        graph().tx().commit();
+        cached.remove();
+        graph().tx().commit();
+        Assert.assertFalse(graph().vertices(original.id()).hasNext());
+    }
+
+    @Test
+    public void testUpdateCachedEdgeAfterIndexRemoval() throws Exception {
+        super.initPropertyKeys();
+        SchemaManager schema = graph().schema();
+        schema.vertexLabel("author").properties("id", "name").primaryKeys("id").create();
+        schema.vertexLabel("book").properties("name").primaryKeys("name").create();
+        schema.edgeLabel("authored").singleTime().link("author", "book").properties("contribution").create();
+        schema.indexLabel("authoredByContri").onE("authored").secondary().by("contribution").create();
+        Vertex author = graph().addVertex(T.label, "author", "id", 1, "name", "author");
+        Vertex book = graph().addVertex(T.label, "book", "name", "book");
+        Edge original = author.addEdge("authored", book, "contribution", "old");
+        graph().tx().commit();
+        Edge cached = graph().edges(original.id()).next();
+        graph().tx().commit();
+        HugeGraphParams params = Whitebox.getInternalState(graph(), "params");
+        params.schemaEventHub().notify(Events.CACHE, "clear", null).get();
+
+        schema.indexLabel("authoredByContri").remove();
+        Assert.assertThrows(NotFoundException.class, () -> schema.getIndexLabel("authoredByContri"));
+        cached.property("contribution", "new");
+        graph().tx().commit();
+        Assert.assertEquals("new", graph().edges(original.id()).next().value("contribution"));
+        graph().tx().commit();
+        schema.indexLabel("authoredByContri").onE("authored").secondary().by("contribution").create();
+        graph().taskScheduler().waitUntilAllTasksCompleted(30);
+        cached.property("contribution", "newer");
+        graph().tx().commit();
+        Assert.assertFalse(graph().traversal().E().has("contribution", "new").hasNext());
+        Assert.assertEquals(original.id(), graph().traversal().E().has("contribution", "newer").next().id());
+        graph().tx().commit();
+        cached.remove();
+        graph().tx().commit();
+        Assert.assertFalse(graph().edges(original.id()).hasNext());
+    }
+
     @Test
     public void testRemoveIndexLabelOfEdge() {
         super.initPropertyKeys();
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 b6657ae15..5add98f07 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
@@ -31,6 +31,7 @@ import java.util.Objects;
 import java.util.Random;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.hugegraph.exception.HugeException;
@@ -72,6 +73,7 @@ import org.apache.hugegraph.type.define.WriteType;
 import org.apache.hugegraph.util.Blob;
 import org.apache.hugegraph.util.CollectionUtil;
 import org.apache.hugegraph.util.DateUtil;
+import org.apache.hugegraph.util.Events;
 import org.apache.hugegraph.util.LongEncoding;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
@@ -198,6 +200,23 @@ public class VertexCoreTest extends BaseCoreTest {
               .create();
     }
 
+    @Test
+    public void testDeleteVertexWithMissingSchemaLabel() throws InterruptedException, ExecutionException {
+        HugeGraph graph = graph();
+        Vertex vertex = graph.addVertex(T.label, "author", "id", 1, "name", "James");
+        graph.tx().commit();
+
+        // Simulate schema loss while the committed backend record remains.
+        params().schemaTransaction().removeSchema(graph.vertexLabel("author"));
+        params().graphEventHub().notify(Events.CACHE, "clear", null).get();
+        Vertex orphan = graph.vertices(vertex.id()).next();
+        Assert.assertEquals("~undefined", orphan.label());
+
+        orphan.remove();
+        graph.tx().commit();
+        Assert.assertFalse(graph.vertices(vertex.id()).hasNext());
+    }
+
     @Test
     public void testAddVertex() {
         HugeGraph graph = graph();