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/05 17:02:31 UTC

(hugegraph) branch master updated: refactor(server): preserve core gremlin semantics (2/3) (#3262)

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


The following commit(s) were added to refs/heads/master by this push:
     new 8fd9841eb refactor(server): preserve core gremlin semantics (2/3) (#3262)
8fd9841eb is described below

commit 8fd9841ebc6770df6119fdce07249fa9e558ae9e
Author: contrueCT <co...@outlook.com>
AuthorDate: Tue Oct 6 01:02:25 2026 +0800

    refactor(server): preserve core gremlin semantics (2/3) (#3262)
    
    Part 2 of upgrade series. Preserve transaction count and predicate semantics while retaining TinkerPop 3.5.1
    
    Stabilize query behavior on TinkerPop 3.5.1: pending removals take precedence in transaction-visible counts, unsupported mixed filters remain in one local HasStep, and count barriers, input multiplicity and iterator cleanup are preserved.
    
    ---------
    
    Co-authored-by: imbajin <ji...@apache.org>
---
 docs/query-semantics.md                            |  41 +
 .../hugegraph/backend/tx/GraphTransaction.java     |  71 +-
 .../traversal/optimize/HugeCountStep.java          |  10 +-
 .../traversal/optimize/HugeCountStepStrategy.java  |   9 +-
 .../traversal/optimize/HugeGraphStep.java          |   3 +-
 .../traversal/optimize/TraversalUtil.java          |  66 +-
 .../hugegraph/core/CountStrategyCoreTest.java      | 865 ++++++++++++++++++++-
 .../optimize/TraversalUtilOptimizeTest.java        |  98 ++-
 8 files changed, 1131 insertions(+), 32 deletions(-)

diff --git a/docs/query-semantics.md b/docs/query-semantics.md
new file mode 100644
index 000000000..5cfb410ce
--- /dev/null
+++ b/docs/query-semantics.md
@@ -0,0 +1,41 @@
+# Query count and filter behavior
+
+Counts made inside a transaction include that transaction's uncommitted vertex
+and edge changes. Only `count()` supports this fallback; other aggregates with
+uncommitted changes are rejected. Queries that combine uncommitted changes with
+pagination, a limit, or an offset remain unsupported.
+
+Queries and counts select the current transaction version before applying filters.
+When an element is removed and re-added with the same ID, the new element shadows
+the old updated record, even if only the old record matches the query's label.
+
+A count query that enters an index query path remains unsupported when that index
+transaction has uncommitted changes. It reports `Can't do index query when there
+are changes in transaction`. This includes indexed-property conditions and
+label-only queries on backends that need a label index. Graph-wide and explicit-ID
+counts can still use transaction merging without entering that index path.
+
+Unsupported text predicates remain traversal filters. For ordinary GraphStep and
+VertexStep extraction, if any condition in a `HasStep` cannot be converted, the
+whole step remains local, including its sibling label, ID and property conditions.
+Count optimization preserves that filter step. Existing special handling around
+`match()` and connective label filters is inherited from master.
+
+New generalized selective pushdown, predicate-specific local ID/SEARCH matching and
+candidate-index coverage are outside this upgrade series. They are coordinated
+through [PR #2994](https://github.com/apache/hugegraph/pull/2994),
+[issue #3201](https://github.com/apache/hugegraph/issues/3201) and
+[PR #3243](https://github.com/apache/hugegraph/pull/3243).
+
+A self-loop contributes two occurrences to a vertex's `bothE()` traversal and
+one to each of `outE()`, `inE()` and the graph-wide `E()` traversal. Counts retain
+these multiplicities before and after transaction commit, including edge updates.
+
+Count optimization preserves steps that can filter candidates. Resetting an
+optimized count traversal permits it to execute again. Query-step equality does
+not depend on the result iterator from a previous execution.
+
+A scan in the middle of a traversal is counted once per incoming traverser,
+including its bulk. It is not replaced by a single backend count. For example,
+with three vertices, `g.V().V().count()` returns `9L`, while `g.V().count()`
+returns `3L` and remains eligible for count optimization.
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 b22fb1dad..77ea7784e 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
@@ -64,6 +64,7 @@ import org.apache.hugegraph.exception.NotFoundException;
 import org.apache.hugegraph.iterator.BatchMapperIterator;
 import org.apache.hugegraph.iterator.ExtendableIterator;
 import org.apache.hugegraph.iterator.FilterIterator;
+import org.apache.hugegraph.iterator.FlatMapperIterator;
 import org.apache.hugegraph.iterator.LimitIterator;
 import org.apache.hugegraph.iterator.MapperIterator;
 import org.apache.hugegraph.job.system.DeleteExpiredJob;
@@ -548,24 +549,27 @@ public class GraphTransaction extends IndexableTransaction {
         boolean hasUpdate = this.hasUpdate();
         Aggregate aggregate = query.aggregateNotNull();
 
-        // TODO: we can concat index-query results and tx uncommitted records.
-        if (hasUpdate) {
-            E.checkArgument(!isConditionQuery,
-                            "It's not allowed to query by index when " +
-                            "there are uncommitted records.");
+        if (hasUpdate && (query.resultType() == HugeType.VERTEX ||
+                          query.resultType() == HugeType.EDGE)) {
+            E.checkArgument(aggregate.func() == AggregateFunc.COUNT,
+                            "The %s operator with uncommitted records " +
+                            "is not supported",
+                            aggregate.func().string());
+            Query queryWithoutAggregate = query.copy();
+            queryWithoutAggregate.aggregate(null);
+            Iterator<?> results = queryWithoutAggregate.resultType().isVertex() ?
+                                  this.queryVertices(queryWithoutAggregate) :
+                                  this.queryEdges(queryWithoutAggregate);
+            return countAndClose(results);
         }
 
         QueryList<Number> queries = this.optimizeQueries(query, q -> {
             boolean isIndexQuery = q instanceof IdQuery;
             assert isIndexQuery || isConditionQuery || q == query;
-            // Need to fall back if there are uncommitted records
-            boolean fallback = hasUpdate;
+            boolean fallback = false;
             Number result;
 
-            if (fallback) {
-                // Here just ignore it, and do fall back later
-                result = null;
-            } else if (!isIndexQuery || !isConditionQuery) {
+            if (!isIndexQuery || !isConditionQuery) {
                 // It's a sysprop-query, let parent tx do it
                 assert !fallback;
                 result = super.queryNumber(q);
@@ -591,9 +595,9 @@ public class GraphTransaction extends IndexableTransaction {
                 assert q.resultType().isVertex() || q.resultType().isEdge();
                 // Reset aggregate to fallback and scan
                 q.aggregate(null);
-                result = IteratorUtils.count(q.resultType().isVertex() ?
-                                             this.queryVertices(q) :
-                                             this.queryEdges(q));
+                result = countAndClose(q.resultType().isVertex() ?
+                                       this.queryVertices(q) :
+                                       this.queryEdges(q));
             }
 
             return new QueryResults<>(IteratorUtils.of(result), q);
@@ -605,6 +609,19 @@ public class GraphTransaction extends IndexableTransaction {
         return aggregate.reduce(results.iterator());
     }
 
+    private static long countAndClose(Iterator<?> results) {
+        try {
+            long count = 0L;
+            while (results.hasNext()) {
+                results.next();
+                count++;
+            }
+            return count;
+        } finally {
+            CloseableIterator.closeIterator(results);
+        }
+    }
+
     @Watched(prefix = "graph")
     public HugeVertex addVertex(Object... keyValues) {
         return this.addVertex(this.constructVertex(true, keyValues));
@@ -819,7 +836,7 @@ public class GraphTransaction extends IndexableTransaction {
     public Iterator<Vertex> queryVertices(Query query) {
         if (this.hasUpdate()) {
             E.checkArgument(query.noLimitAndOffset(),
-                            "It's not allowed to query with offser/limit " +
+                            "It's not allowed to query with offset/limit " +
                             "when there are uncommitted records.");
             // TODO: also add check: no SCAN, no OLAP
             E.checkArgument(!query.paging(),
@@ -1008,7 +1025,7 @@ public class GraphTransaction extends IndexableTransaction {
     public Iterator<Edge> queryEdges(Query query) {
         if (this.hasUpdate()) {
             E.checkArgument(query.noLimitAndOffset(),
-                            "It's not allowed to query with offser/limit " +
+                            "It's not allowed to query with offset/limit " +
                             "when there are uncommitted records.");
             // TODO: also add check: no SCAN, no OLAP
             E.checkArgument(!query.paging(),
@@ -2037,6 +2054,22 @@ public class GraphTransaction extends IndexableTransaction {
         edges = this.joinTxRecords(query, edges, matchTxFunc,
                                    this.addedEdges, this.removedEdges,
                                    this.updatedEdges);
+        if (query instanceof ConditionQuery &&
+            ((ConditionQuery) query).containsCondition(HugeKeys.OWNER_VERTEX) &&
+            !(this.addedEdges.isEmpty() && this.updatedEdges.isEmpty())) {
+            edges = new FlatMapperIterator<>(edges, edge -> {
+                // Rehydrated self-loops may hold distinct vertex objects.
+                if (edge.sourceVertex().id().equals(edge.targetVertex().id()) &&
+                    (this.addedEdges.containsKey(edge.id()) ||
+                     this.updatedEdges.containsKey(edge.id()))) {
+                    HugeEdge opposite = edge.switchOwner();
+                    if (query.test(opposite)) {
+                        return ImmutableList.of(edge, opposite).iterator();
+                    }
+                }
+                return ImmutableList.of(edge).iterator();
+            });
+        }
         if (removingVertices.isEmpty()) {
             return edges;
         }
@@ -2073,6 +2106,9 @@ public class GraphTransaction extends IndexableTransaction {
          * Records in memory have higher priority than a query from backend store
          */
         for (V elem : addedTxRecords.values()) {
+            if (removedTxRecords.containsKey(elem.id())) {
+                continue;
+            }
             if (query.reachLimit(txResults.size())) {
                 break;
             }
@@ -2081,6 +2117,9 @@ public class GraphTransaction extends IndexableTransaction {
             }
         }
         for (V elem : updatedTxRecords.values()) {
+            if (removedTxRecords.containsKey(elem.id()) || addedTxRecords.containsKey(elem.id())) {
+                continue;
+            }
             if (query.reachLimit(txResults.size())) {
                 break;
             }
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
index 403bf5be8..f12a84e40 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
@@ -55,12 +55,18 @@ public final class HugeCountStep<S extends Element>
 
         HugeCountStep other = (HugeCountStep) obj;
         return Objects.equals(this.originGraphStep,
-                              other.originGraphStep) && this.done == other.done;
+                              other.originGraphStep);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(super.hashCode(), this.originGraphStep, this.done);
+        return Objects.hash(super.hashCode(), this.originGraphStep);
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        this.done = false;
     }
 
     @Override
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java
index ef380d36b..b4ef2500e 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java
@@ -34,7 +34,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateG
 import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateLocalStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectStep;
-import org.apache.tinkerpop.gremlin.process.traversal.step.util.CollectingBarrierStep;
 import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
 import org.apache.tinkerpop.gremlin.structure.Element;
@@ -73,11 +72,11 @@ public final class HugeCountStepStrategy
         HugeGraphStep<?, ? extends Element> graphStep = null;
         Step<?, ?> step = originStep;
         do {
+            // Only skip steps that preserve the number of traversers.
             if (!(step instanceof CountGlobalStep ||
                   step instanceof GraphStep ||
                   step instanceof IdentityStep ||
-                  step instanceof NoOpBarrierStep ||
-                  step instanceof CollectingBarrierStep) ||
+                  step instanceof NoOpBarrierStep) ||
                 (step instanceof TraversalParent &&
                  TraversalHelper.anyStepRecursively(s -> {
                      return s instanceof SideEffectStep ||
@@ -94,7 +93,9 @@ public final class HugeCountStepStrategy
             step = step.getPreviousStep();
         } while (step != null);
 
-        if (graphStep == null) {
+        // A mid-traversal scan runs once per incoming traverser (including bulk).
+        if (graphStep == null || !graphStep.isStartStep() ||
+            traversal.getStartStep() != graphStep) {
             return;
         }
 
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java
index bdfb9e0b6..48381241a 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java
@@ -210,8 +210,7 @@ public final class HugeGraphStep<S, E extends Element>
 
         HugeGraphStep other = (HugeGraphStep) obj;
         return this.hasContainers.equals(other.hasContainers) &&
-               this.queryInfo.equals(other.queryInfo) &&
-               this.lastTimeResults.equals(other.lastTimeResults);
+               this.queryInfo.equals(other.queryInfo);
     }
 
     @Override
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
index e6a56027a..6a9f9394d 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
@@ -60,6 +60,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Contains;
 import org.apache.tinkerpop.gremlin.process.traversal.Order;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
@@ -177,7 +178,8 @@ public final class TraversalUtil {
         while (step instanceof HasStep || step instanceof NoOpBarrierStep) {
             Step<?, ?> nextStep = step.getNextStep();
             if (step instanceof HasStep) {
-                HasContainerHolder holder = (HasContainerHolder) step;
+                HasContainerHolder holder =
+                        (HasContainerHolder) step;
                 boolean connectiveLabelStep =
                         removeConnectiveLabelStep(step);
                 /*
@@ -385,7 +387,7 @@ public final class TraversalUtil {
     }
 
     static boolean isPositiveLabelContainer(HasContainer has) {
-        if (!isLabelContainer(has)) {
+        if (!isLabelContainer(has) || hasNullLabelValue(has)) {
             return false;
         }
 
@@ -541,6 +543,29 @@ public final class TraversalUtil {
         return false;
     }
 
+    private static boolean hasNullLabelValue(HasContainer has) {
+        if (!isLabelContainer(has)) {
+            return false;
+        }
+
+        List<P<Object>> predicates = new ArrayList<>();
+        collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+        for (P<Object> pred : predicates) {
+            Object value = pred.getValue();
+            if (value == null) {
+                return true;
+            }
+            if (value instanceof Collection) {
+                for (Object label : (Collection<?>) value) {
+                    if (label == null) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
     private static boolean hasBooleanIndex(HugeGraph graph,
                                            SchemaLabel schemaLabel,
                                            PropertyKey pkey) {
@@ -607,7 +632,8 @@ public final class TraversalUtil {
             Step<?, ?> nextStep = step.getNextStep();
             if (step instanceof HasStep) {
                 removeConnectiveLabelStep(step);
-                HasContainerHolder holder = (HasContainerHolder) step;
+                HasContainerHolder holder =
+                        (HasContainerHolder) step;
                 if (extractHasContainers(newStep, holder)) {
                     TraversalHelper.copyLabels(step, step.getPreviousStep(), false);
                     traversal.removeStep(step);
@@ -655,6 +681,11 @@ public final class TraversalUtil {
 
     static boolean canExtractHasContainer(HugeGraph graph,
                                           HasContainer has) {
+        if (has.getKey() == null || has.getPredicate() == null ||
+            hasNullLabelValue(has) ||
+            hasTextPredicate(has)) {
+            return false;
+        }
         if (isSysProp(has.getKey())) {
             return true;
         }
@@ -687,6 +718,17 @@ public final class TraversalUtil {
         return true;
     }
 
+    private static boolean hasTextPredicate(HasContainer has) {
+        List<P<Object>> predicates = new ArrayList<>();
+        collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+        for (P<Object> predicate : predicates) {
+            if (TextP.class.isInstance(predicate)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static void extractOrder(Step<?, ?> newStep,
                                     Traversal.Admin<?, ?> traversal) {
         Step<?, ?> step = newStep;
@@ -1097,6 +1139,9 @@ public final class TraversalUtil {
     }
 
     public static HugeKeys token2HugeKey(String key) {
+        if (key == null) {
+            return null;
+        }
         if (key.equals(T.label.getAccessor())) {
             return HugeKeys.LABEL;
         } else if (key.equals(T.id.getAccessor())) {
@@ -1187,7 +1232,7 @@ public final class TraversalUtil {
     private static void convPredicateValue(HugeGraph graph,
                                            HasContainer has) {
         // No need to convert if key is sys-prop
-        if (isSysProp(has.getKey())) {
+        if (has.getKey() == null || isSysProp(has.getKey())) {
             return;
         }
         PropertyKey pkey = graph.propertyKey(has.getKey());
@@ -1198,8 +1243,7 @@ public final class TraversalUtil {
         List<P<Object>> leafPredicates = new ArrayList<>();
         collectPredicates(leafPredicates, ImmutableList.of(predicate));
         for (P<Object> pred : leafPredicates) {
-            if (pred.getBiPredicate() == Compare.neq &&
-                pred.getValue() == null) {
+            if (isNullInequalityPredicate(pred)) {
                 continue;
             }
             Object value = validPropertyValue(pred.getValue(), pkey);
@@ -1207,6 +1251,16 @@ public final class TraversalUtil {
         }
     }
 
+    private static boolean isNullInequalityPredicate(P<?> predicate) {
+        if (predicate.getValue() != null) {
+            return false;
+        }
+        if (predicate.getBiPredicate() == Compare.neq) {
+            return true;
+        }
+        return false;
+    }
+
     private static boolean isSysProp(String key) {
         if (QueryHolder.SYSPROP_PAGE.equals(key)) {
             return true;
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
index f00918050..693b2404b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
@@ -17,23 +17,53 @@
 
 package org.apache.hugegraph.core;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.hugegraph.HugeException;
+import org.apache.hugegraph.backend.query.Aggregate;
+import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc;
+import org.apache.hugegraph.backend.query.Condition;
+import org.apache.hugegraph.backend.query.ConditionQuery;
+import org.apache.hugegraph.backend.query.Query;
+import org.apache.hugegraph.backend.tx.GraphTransaction;
 import org.apache.hugegraph.exception.NoIndexException;
 import org.apache.hugegraph.schema.SchemaManager;
 import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.traversal.optimize.ConditionP;
+import org.apache.hugegraph.traversal.optimize.HugeCountStep;
+import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy;
+import org.apache.hugegraph.traversal.optimize.HugeCountStrategy;
 import org.apache.hugegraph.traversal.optimize.HugeGraphStep;
+import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy;
+import org.apache.hugegraph.type.HugeType;
+import org.apache.hugegraph.type.define.HugeKeys;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
 import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
 import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.T;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
 import org.junit.Test;
 
 public class CountStrategyCoreTest extends BaseCoreTest {
@@ -101,7 +131,8 @@ public class CountStrategyCoreTest extends BaseCoreTest {
             if (!(step instanceof HasStep)) {
                 continue;
             }
-            HasContainerHolder holder = (HasContainerHolder) step;
+            HasContainerHolder holder =
+                    (HasContainerHolder) step;
             for (HasContainer has : holder.getHasContainers()) {
                 if (key.equals(has.getKey())) {
                     return true;
@@ -111,6 +142,38 @@ public class CountStrategyCoreTest extends BaseCoreTest {
         return false;
     }
 
+    private void assertNegatedBooleanPredicate(long expected,
+                                                P<Boolean> predicate) {
+        GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
+                                                        .has("vp2",
+                                                             P.not(predicate))
+                                                        .count();
+        traversal.asAdmin().applyStrategies();
+
+        Assert.assertTrue(hasRemainingHasStep(traversal, "vp2"));
+        Assert.assertEquals(expected, traversal.next().longValue());
+    }
+
+    private static void assertUncommittedRangeUnsupported(
+            GraphTraversal<?, ?> traversal) {
+        Assert.assertThrows(IllegalArgumentException.class, traversal::next,
+                            e -> {
+                                Assert.assertContains("offset/limit", e.getMessage());
+                                Assert.assertContains("uncommitted records", e.getMessage());
+                            });
+    }
+
+    private static void assertNegatedCountHighRange(long expected,
+                                                    P<Long> predicate) {
+        GraphTraversal<?, Long> traversal = __.count().is(P.not(predicate));
+        HugeCountStrategy.instance().apply(traversal.asAdmin());
+
+        Step<?, ?> firstStep = traversal.asAdmin().getStartStep();
+        Assert.assertInstanceOf(RangeGlobalStep.class, firstStep);
+        Assert.assertEquals(expected,
+                            ((RangeGlobalStep<?>) firstStep).getHighRange());
+    }
+
     private void initTextRangeSchema(boolean withEdge) {
         SchemaManager schema = graph().schema();
         schema.propertyKey("vp4").asText().create();
@@ -134,6 +197,89 @@ public class CountStrategyCoreTest extends BaseCoreTest {
               .nullableKeys("ep4").link("vl1", "vl1").create();
     }
 
+    private void initNegatedDoubleSchema() {
+        SchemaManager schema = graph().schema();
+        schema.propertyKey("score").asDouble().create();
+        schema.vertexLabel("sample").properties("score").create();
+        schema.indexLabel("sampleByScore").onV("sample")
+              .by("score").range().create();
+    }
+
+    private static void assertUnoptimizedCount(long expected,
+                                                GraphTraversal<?, Long> traversal) {
+        traversal.asAdmin().applyStrategies();
+
+        Assert.assertInstanceOf(CountGlobalStep.class, traversal.asAdmin().getEndStep());
+        Assert.assertFalse(traversal.asAdmin().getSteps().stream()
+                                   .anyMatch(step -> step instanceof HugeCountStep));
+        Assert.assertEquals(Collections.singletonList(expected), traversal.toList());
+    }
+
+    private void assertRepeatedGraphCounts(long vertices) {
+        assertUnoptimizedCount(vertices * vertices,
+                               graph().traversal().V().V().count());
+        assertUnoptimizedCount(2L * vertices,
+                               graph().traversal().inject(1, 2).V().count());
+        // Repeated inputs can be bulked, but each must still contribute a scan.
+        assertUnoptimizedCount(2L * vertices,
+                               graph().traversal().inject(1, 1).barrier().V().count());
+
+        GraphTraversal<Vertex, Long> root = graph().traversal().V().count();
+        root.asAdmin().applyStrategies();
+        Assert.assertInstanceOf(HugeCountStep.class, root.asAdmin().getStartStep());
+        Assert.assertEquals(Collections.singletonList(vertices), root.toList());
+    }
+
+    @Test
+    public void testRepeatedGraphCountOnEmptyGraph() {
+        this.initSchema();
+
+        this.assertRepeatedGraphCounts(0L);
+    }
+
+    @Test
+    public void testRepeatedGraphCountOnSingleVertex() {
+        this.initSchema();
+        graph().addVertex(T.label, "person", "name", "marko");
+        commitTx();
+
+        this.assertRepeatedGraphCounts(1L);
+    }
+
+    @Test
+    public void testRepeatedGraphCountOnMultipleVertices() {
+        this.initSchema();
+        this.initGraph();
+
+        this.assertRepeatedGraphCounts(3L);
+    }
+
+    @Test
+    public void testRepeatedGraphCountWithIds() {
+        this.initSchema();
+        this.initGraph();
+        Object id = graph().traversal().V().next().id();
+
+        assertUnoptimizedCount(3L, graph().traversal().V().V(id).count());
+        assertUnoptimizedCount(2L, graph().traversal().inject(1, 2).V(id).count());
+        assertUnoptimizedCount(1L, graph().traversal().V(id).V(id).count());
+    }
+
+    @Test
+    public void testCountAfterOrderByPresentProperty() {
+        this.initSchema();
+        this.initGraph();
+
+        GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
+                                                        .order().by("name").count();
+        traversal.asAdmin().applyStrategies();
+
+        Assert.assertFalse(traversal.asAdmin().getEndStep() instanceof HugeCountStep);
+        Assert.assertTrue(traversal.asAdmin().getSteps().stream()
+                                   .anyMatch(step -> step instanceof OrderGlobalStep));
+        Assert.assertEquals(3L, traversal.next().longValue());
+    }
+
     @Test
     public void testWhereCountLtNegativeIsAlwaysFalse() {
         this.initSchema();
@@ -246,6 +392,63 @@ public class CountStrategyCoreTest extends BaseCoreTest {
         Assert.assertEquals(1L, count);
     }
 
+    @Test
+    public void testOptimizedGraphCountCanBeResetAndReused() {
+        this.initSchema();
+        this.initGraph();
+
+        GraphTraversal<Vertex, Long> traversal = graph().traversal().V().count();
+
+        Assert.assertEquals(3L, traversal.next());
+
+        traversal.asAdmin().reset();
+
+        Assert.assertEquals(3L, traversal.next());
+    }
+
+    @Test
+    public void testOptimizedGraphCountEqualityIgnoresExecutionState() {
+        this.initSchema();
+        this.initGraph();
+
+        GraphTraversal<Vertex, Long> first = graph().traversal().V().count();
+        GraphTraversal<Vertex, Long> second = graph().traversal().V().count();
+        first.asAdmin().applyStrategies();
+        second.asAdmin().applyStrategies();
+
+        Step<?, ?> firstStep = first.asAdmin().getEndStep();
+        Step<?, ?> secondStep = second.asAdmin().getEndStep();
+        Assert.assertInstanceOf(HugeCountStep.class, firstStep);
+        Assert.assertInstanceOf(HugeCountStep.class, secondStep);
+        Assert.assertEquals(firstStep, secondStep);
+
+        int hashCode = firstStep.hashCode();
+        Set<Step<?, ?>> steps = new HashSet<>();
+        steps.add(firstStep);
+
+        Assert.assertEquals(3L, first.next());
+
+        Assert.assertEquals(hashCode, firstStep.hashCode());
+        Assert.assertEquals(firstStep, secondStep);
+        Assert.assertTrue(steps.contains(firstStep));
+    }
+
+    @Test
+    public void testOptimizedGraphCountIncludesUncommittedRecords() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+
+        graph().addVertex(T.label, "person", "name", "marko");
+
+        long count = graph().traversal().V()
+                            .hasLabel("person")
+                            .has("name", "marko")
+                            .count().next();
+
+        Assert.assertEquals(1L, count);
+    }
+
     @Test
     public void testWhereCountFlatAndContradictionEmpty() {
         this.initSchema();
@@ -338,6 +541,666 @@ public class CountStrategyCoreTest extends BaseCoreTest {
         Assert.assertEquals(1L, count);
     }
 
+    @Test
+    public void testVertexLimitCountRejectsUncommittedAddition() {
+        this.initSchema();
+        graph().addVertex(T.label, "person", "name", "marko");
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().V().limit(1L).count());
+    }
+
+    @Test
+    public void testVertexRangeCountRejectsUncommittedDeletion() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+        this.initGraph();
+        Vertex marko = graph().traversal().V()
+                              .hasLabel("person")
+                              .has("name", "marko")
+                              .next();
+        marko.remove();
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().V().range(1L, 3L).count());
+    }
+
+    @Test
+    public void testSystemCountsIgnoreUncommittedVertices() {
+        this.initSchema();
+        for (HugeType type : new HugeType[]{HugeType.TASK, HugeType.SERVER}) {
+            Query query = new Query(type);
+            query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+            long before = graph().queryNumber(query).longValue();
+            graph().addVertex(T.label, "person", "name", "uncommitted");
+            Assert.assertEquals(before, graph().queryNumber(query).longValue());
+            graph().tx().rollback();
+        }
+    }
+
+    @Test
+    public void testQueryNumberKeepsOriginalAggregate() {
+        this.initSchema();
+        graph().addVertex(T.label, "person", "name", "marko");
+
+        Query query = new Query(HugeType.VERTEX);
+        Aggregate aggregate = new Aggregate(AggregateFunc.COUNT, null);
+        query.aggregate(aggregate);
+
+        Assert.assertEquals(1L, graph().queryNumber(query).longValue());
+        Assert.assertSame(aggregate, query.aggregate());
+    }
+
+    @Test
+    public void testUncommittedVertexCountClosesIteratorOnFailure() {
+        CountCloseableIterator<Vertex> vertices =
+                new CountCloseableIterator<>(true);
+        AtomicBoolean dirty = new AtomicBoolean(true);
+        GraphTransaction transaction =
+                this.newCountTransaction(vertices, null, dirty);
+
+        try {
+            Query query = countQuery(HugeType.VERTEX);
+            Assert.assertThrows(IllegalStateException.class,
+                                () -> transaction.queryNumber(query));
+            Assert.assertTrue(vertices.closed());
+        } finally {
+            dirty.set(false);
+            transaction.close();
+        }
+    }
+
+    @Test
+    public void testUncommittedEdgeCountClosesIteratorOnFailure() {
+        CountCloseableIterator<Edge> edges =
+                new CountCloseableIterator<>(true);
+        AtomicBoolean dirty = new AtomicBoolean(true);
+        GraphTransaction transaction =
+                this.newCountTransaction(null, edges, dirty);
+
+        try {
+            Query query = countQuery(HugeType.EDGE);
+            Assert.assertThrows(IllegalStateException.class,
+                                () -> transaction.queryNumber(query));
+            Assert.assertTrue(edges.closed());
+        } finally {
+            dirty.set(false);
+            transaction.close();
+        }
+    }
+
+    @Test
+    public void testCommittedVertexCountClosesIterator() {
+        this.assertCommittedVertexCountClosesIterator(false);
+    }
+
+    @Test
+    public void testCommittedVertexCountClosesIteratorOnFailure() {
+        this.assertCommittedVertexCountClosesIterator(true);
+    }
+
+    private void assertCommittedVertexCountClosesIterator(boolean fail) {
+        SchemaManager schema = graph().schema();
+        schema.propertyKey("name").asText().create();
+        schema.vertexLabel("person").properties("name")
+              .primaryKeys("name").create();
+
+        ConditionQuery query = new ConditionQuery(HugeType.VERTEX);
+        query.eq(HugeKeys.LABEL, graph().vertexLabel("person").id());
+        query.query(Condition.eq(graph().propertyKey("name").id(), "marko"));
+        query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+
+        CountCloseableIterator<Vertex> vertices = new CountCloseableIterator<>(fail);
+        GraphTransaction transaction = this.newCountTransaction(
+                vertices, null, new AtomicBoolean(false));
+        try {
+            // Primary-key optimization produces an IdQuery that must scan to count.
+            if (fail) {
+                Assert.assertThrows(IllegalStateException.class,
+                                    () -> transaction.queryNumber(query));
+            } else {
+                Assert.assertEquals(1L, transaction.queryNumber(query).longValue());
+            }
+            Assert.assertTrue(vertices.closed());
+        } finally {
+            transaction.close();
+        }
+    }
+
+    @Test
+    public void testLabelPredicatesWithNullHostileCollections() {
+        this.initSchema();
+        Vertex person = graph().addVertex(T.label, "person", "name", "marko");
+        Vertex software = graph().addVertex(T.label, "software", "name", "market");
+        commitTx();
+
+        List<Collection<String>> values = List.of(List.of("person"), Set.of("person"));
+        for (Collection<String> labels : values) {
+            Assert.assertEquals(Collections.singletonList(person),
+                                graph().traversal().V().hasLabel(P.within(labels)).toList());
+            Assert.assertEquals(1L, graph().traversal().V().hasLabel(P.within(labels))
+                                         .count().next().longValue());
+            GraphTraversal<Vertex, Vertex> without = graph().traversal().V()
+                    .hasLabel(P.without(labels)).has("name", TextP.containing("ar"));
+            without.asAdmin().applyStrategies();
+            Assert.assertTrue(hasRemainingHasStep(without, T.label.getAccessor()));
+            Assert.assertEquals(Collections.singletonList(software), without.toList());
+        }
+    }
+
+    @Test
+    public void testLabelPredicatesWithMutableNullValuesStayLocal() {
+        this.initSchema();
+        Vertex person = graph().addVertex(T.label, "person", "name", "marko");
+        Vertex software = graph().addVertex(T.label, "software", "name", "market");
+        commitTx();
+
+        Collection<String> list = new ArrayList<>(List.of("person"));
+        list.add(null);
+        Collection<String> set = new HashSet<>(list);
+        for (Collection<String> labels : List.of(list, set)) {
+            GraphTraversal<Vertex, Vertex> within = graph().traversal().V()
+                    .hasLabel(P.within(labels));
+            within.asAdmin().applyStrategies();
+            Assert.assertTrue(hasRemainingHasStep(within, T.label.getAccessor()));
+            Assert.assertEquals(Collections.singletonList(person), within.toList());
+            GraphTraversal<Vertex, Vertex> without = graph().traversal().V()
+                    .hasLabel(P.without(labels));
+            without.asAdmin().applyStrategies();
+            Assert.assertTrue(hasRemainingHasStep(without, T.label.getAccessor()));
+            Assert.assertEquals(Collections.singletonList(software), without.toList());
+        }
+    }
+
+    @Test
+    public void testMixedTextNativeIdFilterKeepsAllConditionsLocal() {
+        this.initSchema();
+        Vertex josh = graph().addVertex(T.label, "person", "name", "josh");
+        Vertex marko = graph().addVertex(T.label, "person", "name", "marko");
+        commitTx();
+
+        GraphTraversalSource[] sources = {
+                graph().traversal(),
+                graph().traversal().withoutStrategies(HugeGraphStepStrategy.class,
+                                                     HugeCountStepStrategy.class)
+        };
+        for (GraphTraversalSource source : sources) {
+            assertMixedTextNativeIdFilter(source, marko.id(), "ar",
+                                         Collections.singletonList(marko));
+            assertMixedTextNativeIdFilter(source, josh.id(), "ar",
+                                         Collections.emptyList());
+            assertMixedTextNativeIdFilter(source, marko.id(), "osh",
+                                         Collections.emptyList());
+            Assert.assertEquals(Collections.singletonList(marko),
+                                source.V().hasId(marko.id()).toList());
+            Assert.assertEquals(1L, source.V().hasId(P.eq(marko.id()))
+                                         .count().next().longValue());
+        }
+    }
+
+    private static void assertMixedTextNativeIdFilter(GraphTraversalSource source,
+                                                       Object id, String text,
+                                                       List<Vertex> expected) {
+        GraphTraversal<Vertex, Vertex> traversal = nativeIdTextQuery(source, id, text);
+        traversal.asAdmin().applyStrategies();
+        assertMixedTextNativeIdPlan(traversal);
+        Assert.assertEquals(expected, traversal.toList());
+
+        GraphTraversal<Vertex, Long> count = nativeIdTextQuery(source, id, text).count();
+        assertUnoptimizedCount(expected.size(), count);
+        assertMixedTextNativeIdPlan(count);
+        Assert.assertEquals(expected, nativeIdTextQuery(source, id, text)
+                                      .limit(1).toList());
+        Assert.assertEquals(Collections.emptyList(),
+                            nativeIdTextQuery(source, id, text).range(1, 2).toList());
+    }
+
+    private static GraphTraversal<Vertex, Vertex> nativeIdTextQuery(
+            GraphTraversalSource source, Object id, String text) {
+        return source.V().hasId(ConditionP.eq(id))
+                     .has("name", TextP.containing(text));
+    }
+
+    private static void assertMixedTextNativeIdPlan(GraphTraversal<?, ?> traversal) {
+        Assert.assertTrue(hasRemainingHasStep(traversal, T.id.getAccessor()));
+        Assert.assertTrue(hasRemainingHasStep(traversal, "name"));
+        Assert.assertEquals(0, ((GraphStep<?, ?>) traversal.asAdmin()
+                                                       .getStartStep()).getIds().length);
+    }
+
+    @Test
+    public void testMixedTextEdgeFilterKeepsAllConditionsLocal() {
+        this.initSchema();
+        graph().schema().propertyKey("weight").asInt().create();
+        graph().schema().edgeLabel("rated").link("person", "person")
+               .properties("name", "weight").create();
+        Vertex source = graph().addVertex(T.label, "person", "name", "source");
+        Vertex target = graph().addVertex(T.label, "person", "name", "target");
+        source.addEdge("rated", target, "name", "marko", "weight", 1);
+        Vertex other = graph().addVertex(T.label, "person", "name", "other");
+        Edge expected = source.addEdge("rated", other,
+                                       "name", "marko", "weight", 2);
+        commitTx();
+
+        List<Edge> edges = graph().traversal().V(source.id()).outE("rated")
+                                  .has("name", TextP.containing("ar"))
+                                  .has("weight", 2).toList();
+        Assert.assertEquals(1, edges.size());
+        Assert.assertEquals(expected.id(), edges.get(0).id());
+        Assert.assertEquals(1L, graph().traversal().V(source.id()).outE("rated")
+                                      .has("name", TextP.containing("ar"))
+                                      .has("weight", 2).count().next().longValue());
+    }
+
+    @Test
+    public void testUpdatedThenRemovedVertexIsAbsentFromCounts() {
+        this.initSchema();
+        Vertex vertex = graph().addVertex(T.label, "person", "name", "before");
+        commitTx();
+        Object id = vertex.id();
+        vertex = graph().traversal().V(id).next();
+        vertex.property("name", "after");
+        vertex.remove();
+
+        this.assertRemovedVertexCounts(id);
+        graph().tx().rollback();
+        Assert.assertEquals(1L, graph().traversal().V(id).count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().V().hasLabel("person")
+                                      .count().next().longValue());
+        Assert.assertEquals("before", graph().traversal().V(id).values("name").next());
+    }
+
+    private void assertRemovedVertexCounts(Object id) {
+        Assert.assertEquals(0L, graph().traversal().V(id).count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().V().hasLabel("person")
+                                      .count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().V().count().next().longValue());
+        Assert.assertTrue(graph().traversal().V().toList().isEmpty());
+        Assert.assertEquals(0L, graph().traversal().V()
+                                      .has("name", TextP.containing("after"))
+                                      .count().next().longValue());
+    }
+
+    @Test
+    public void testUpdatedThenRemovedEdgeIsAbsentFromCounts() {
+        this.assertUpdatedThenRemovedEdgeCounts(false);
+    }
+
+    @Test
+    public void testUpdatedThenRemovedSelfLoopIsAbsentFromCounts() {
+        this.assertUpdatedThenRemovedEdgeCounts(true);
+    }
+
+    private void assertUpdatedThenRemovedEdgeCounts(boolean selfLoop) {
+        this.initSchema();
+        graph().schema().edgeLabel("rated").link("person", "person")
+               .properties("name").create();
+        Vertex source = graph().addVertex(T.label, "person", "name", "source");
+        Vertex target = selfLoop ? source :
+                        graph().addVertex(T.label, "person", "name", "target");
+        Edge edge = source.addEdge("rated", target, "name", "before");
+        commitTx();
+        Object id = edge.id();
+        edge = graph().traversal().V(target.id()).inE("rated").next();
+        edge.property("name", "after");
+        edge.remove();
+
+        this.assertRemovedEdgeCounts(id, source, target);
+        graph().tx().rollback();
+        Assert.assertEquals(1L, graph().traversal().E(id).count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated")
+                                      .count().next().longValue());
+        Assert.assertEquals(selfLoop ? 2L : 1L,
+                            graph().traversal().V(source.id()).bothE("rated")
+                                   .count().next().longValue());
+        Assert.assertEquals("before", graph().traversal().E(id).values("name").next());
+    }
+
+    private void assertRemovedEdgeCounts(Object id, Vertex source, Vertex target) {
+        Assert.assertEquals(0L, graph().traversal().E(id).count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().V(source.id()).bothE("rated")
+                                      .count().next().longValue());
+        Assert.assertTrue(graph().traversal().V(source.id()).bothE("rated")
+                                 .toList().isEmpty());
+        Assert.assertEquals(0L, graph().traversal().V(source.id()).outE("rated")
+                                      .count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().V(target.id()).inE("rated")
+                                      .count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().E().hasLabel("rated")
+                                      .count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().E().count().next().longValue());
+        Assert.assertTrue(graph().traversal().E().toList().isEmpty());
+    }
+
+    @Test
+    public void testDirtyVertexIndexCountRemainsUnsupported() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName").onV("person")
+               .by("name").secondary().create();
+        Vertex vertex = graph().addVertex(T.label, "person", "name", "before");
+        commitTx();
+        vertex = graph().traversal().V(vertex.id()).next();
+        vertex.property("name", "after");
+
+        Assert.assertEquals(1L, graph().traversal().V(vertex.id())
+                                      .count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().V().count().next().longValue());
+        assertDirtyIndexCountUnsupported(graph().traversal().V()
+                .hasLabel("person").has("name", "before").count());
+        this.assertDirtyIndexLabelCount(graph().traversal().V()
+                .hasLabel("person").count());
+    }
+
+    @Test
+    public void testDirtyEdgeIndexCountRemainsUnsupported() {
+        this.initSchema();
+        graph().schema().edgeLabel("rated").link("person", "person")
+               .properties("name").create();
+        graph().schema().indexLabel("ratedByName").onE("rated")
+               .by("name").secondary().create();
+        Vertex source = graph().addVertex(T.label, "person", "name", "source");
+        Vertex target = graph().addVertex(T.label, "person", "name", "target");
+        Edge edge = source.addEdge("rated", target, "name", "before");
+        commitTx();
+        edge = graph().traversal().E(edge.id()).next();
+        edge.property("name", "after");
+
+        Assert.assertEquals(1L, graph().traversal().E(edge.id())
+                                      .count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().E().count().next().longValue());
+        assertDirtyIndexCountUnsupported(graph().traversal().E()
+                .hasLabel("rated").has("name", "before").count());
+        this.assertDirtyIndexLabelCount(graph().traversal().E()
+                .hasLabel("rated").count());
+    }
+
+    private void assertDirtyIndexLabelCount(GraphTraversal<?, Long> traversal) {
+        if (storeFeatures().supportsQueryByLabel()) {
+            Assert.assertEquals(1L, traversal.next().longValue());
+        } else {
+            assertDirtyIndexCountUnsupported(traversal);
+        }
+    }
+
+    private static void assertDirtyIndexCountUnsupported(GraphTraversal<?, Long> traversal) {
+        Assert.assertThrows(HugeException.class, traversal::next, e -> {
+            Assert.assertContains("Can't do index query when there are " +
+                                  "changes in transaction", e.getMessage());
+        });
+    }
+
+    @Test
+    public void testRemovedVertexCanBeReaddedWithSameId() {
+        this.initSchema();
+        graph().schema().vertexLabel("reused").properties("name")
+               .useCustomizeStringId().create();
+        Vertex vertex = graph().addVertex(T.label, "reused", T.id, "same",
+                                         "name", "before");
+        commitTx();
+        vertex = graph().traversal().V(vertex.id()).next();
+        vertex.property("name", "changed");
+        vertex.remove();
+        Vertex replacement = graph().addVertex(T.label, "reused", T.id, "same",
+                                               "name", "again");
+
+        Assert.assertEquals(1L, graph().traversal().V(replacement.id())
+                                      .count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().V().hasLabel("reused")
+                                      .count().next().longValue());
+        Assert.assertEquals(Collections.singletonList(replacement),
+                            graph().traversal().V().toList());
+        Assert.assertEquals("again", graph().traversal().V(replacement.id())
+                                           .values("name").next());
+    }
+
+    @Test
+    public void testReaddedVertexShadowsUpdatedVertexBeforeLabelMatching() {
+        SchemaManager schema = graph().schema();
+        schema.propertyKey("name").asText().create();
+        schema.vertexLabel("person").properties("name")
+              .useCustomizeStringId().create();
+        schema.vertexLabel("software").properties("name")
+              .useCustomizeStringId().create();
+        Vertex vertex = graph().addVertex(T.label, "person", T.id, "same",
+                                         "name", "before");
+        commitTx();
+        Object id = vertex.id();
+        vertex = graph().traversal().V(id).next();
+        vertex.property("name", "after");
+        vertex.remove();
+        Vertex replacement = graph().addVertex(T.label, "software", T.id, "same",
+                                               "name", "again");
+
+        Assert.assertEquals(0L, graph().traversal().V(id).hasLabel("person")
+                                      .count().next().longValue());
+        Assert.assertTrue(graph().traversal().V(id).hasLabel("person")
+                                 .toList().isEmpty());
+        Assert.assertEquals(0L, graph().traversal().V().hasLabel("person")
+                                      .count().next().longValue());
+        Assert.assertTrue(graph().traversal().V().hasLabel("person")
+                                 .toList().isEmpty());
+        Assert.assertEquals(1L, graph().traversal().V(id).hasLabel("software")
+                                      .count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().V().hasLabel("software")
+                                      .count().next().longValue());
+        Assert.assertEquals(Collections.singletonList(replacement),
+                            graph().traversal().V().hasLabel("software").toList());
+        Assert.assertEquals(Collections.singletonList(replacement),
+                            graph().traversal().V(id).toList());
+        Assert.assertEquals(Collections.singletonList(replacement),
+                            graph().traversal().V().toList());
+        Assert.assertEquals(1L, graph().traversal().V().count().next().longValue());
+        Assert.assertEquals("again", graph().traversal().V(id).values("name").next());
+
+        graph().tx().rollback();
+        Assert.assertEquals(1L, graph().traversal().V().hasLabel("person")
+                                      .count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().V().hasLabel("software")
+                                      .count().next().longValue());
+        Assert.assertEquals("before", graph().traversal().V(id).values("name").next());
+    }
+
+    @Test
+    public void testRemovedEdgeCanBeReaddedWithSameId() {
+        this.initSchema();
+        graph().schema().edgeLabel("rated").link("person", "person")
+               .properties("name").create();
+        Vertex source = graph().addVertex(T.label, "person", "name", "source");
+        Vertex target = graph().addVertex(T.label, "person", "name", "target");
+        Edge edge = source.addEdge("rated", target, "name", "before");
+        commitTx();
+        edge = graph().traversal().E(edge.id()).next();
+        edge.property("name", "changed");
+        edge.remove();
+        Edge replacement = source.addEdge("rated", target, "name", "again");
+        Assert.assertEquals(edge.id(), replacement.id());
+        Assert.assertEquals(1L, graph().traversal().E(replacement.id())
+                                      .count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated")
+                                      .count().next().longValue());
+        Assert.assertEquals(Collections.singletonList(replacement),
+                            graph().traversal().E().toList());
+        Assert.assertEquals("again", graph().traversal().E(replacement.id())
+                                           .values("name").next());
+        Assert.assertTrue(graph().traversal().E().hasLabel("rated")
+                                 .has("name", TextP.containing("changed"))
+                                 .toList().isEmpty());
+        Assert.assertEquals(0L, graph().traversal().E().hasLabel("rated")
+                                      .has("name", TextP.containing("changed"))
+                                      .count().next().longValue());
+        Assert.assertEquals(0L, graph().traversal().E(replacement.id())
+                                      .has("name", TextP.containing("changed"))
+                                      .count().next().longValue());
+        Assert.assertEquals(Collections.singletonList(replacement),
+                            graph().traversal().E().hasLabel("rated")
+                                   .has("name", TextP.containing("again")).toList());
+        Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated")
+                                      .has("name", TextP.containing("again"))
+                                      .count().next().longValue());
+
+        graph().tx().rollback();
+        Assert.assertEquals(1L, graph().traversal().E(replacement.id())
+                                      .count().next().longValue());
+        Assert.assertEquals("before", graph().traversal().E(replacement.id())
+                                            .values("name").next());
+    }
+
+    @Test
+    public void testAddedSelfLoopCountKeepsBothDirections() {
+        this.initSchema();
+        Vertex vertex = graph().addVertex(T.label, "person", "name", "loop");
+        vertex.addEdge("knows", vertex);
+        this.assertSelfLoopCounts(vertex);
+        commitTx();
+        this.assertSelfLoopCounts(vertex);
+    }
+
+    @Test
+    public void testUpdatedSelfLoopCountKeepsBothDirections() {
+        this.initSchema();
+        graph().schema().edgeLabel("rated").link("person", "person")
+               .properties("name").create();
+        Vertex vertex = graph().addVertex(T.label, "person", "name", "loop");
+        Edge loop = vertex.addEdge("rated", vertex, "name", "before");
+        commitTx();
+        loop = graph().traversal().E(loop.id()).next();
+        loop.property("name", "after");
+        this.assertSelfLoopCounts(vertex);
+        commitTx();
+        this.assertSelfLoopCounts(vertex);
+        loop.remove();
+        Assert.assertEquals(0L, graph().traversal().V(vertex.id())
+                                      .bothE().count().next().longValue());
+    }
+
+    private void assertSelfLoopCounts(Vertex vertex) {
+        Assert.assertEquals(2L, graph().traversal().V(vertex.id())
+                                      .bothE().count().next().longValue());
+        Assert.assertEquals(2, graph().traversal().V(vertex.id())
+                                     .bothE().toList().size());
+        Assert.assertEquals(1L, graph().traversal().V(vertex.id())
+                                      .outE().count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().V(vertex.id())
+                                      .inE().count().next().longValue());
+        Assert.assertEquals(1L, graph().traversal().E().count().next().longValue());
+    }
+
+    @Test
+    public void testOptimizedEdgeCountIncludesUncommittedRecords() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+        this.initGraph();
+
+        Vertex josh = graph().traversal().V()
+                             .hasLabel("person").has("name", "josh").next();
+        Vertex marko = graph().traversal().V()
+                              .hasLabel("person").has("name", "marko").next();
+        josh.addEdge("knows", marko);
+
+        long count = graph().traversal().E().hasLabel("knows").count().next();
+
+        Assert.assertEquals(2L, count);
+    }
+
+    private static Query countQuery(HugeType type) {
+        Query query = new Query(type);
+        query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+        return query;
+    }
+
+    private GraphTransaction newCountTransaction(
+            Iterator<Vertex> vertices, Iterator<Edge> edges,
+            AtomicBoolean dirty) {
+        return new GraphTransaction(params(), params().loadGraphStore()) {
+
+            @Override
+            public boolean hasUpdate() {
+                return dirty.get();
+            }
+
+            @Override
+            public Iterator<Vertex> queryVertices(Query query) {
+                return vertices;
+            }
+
+            @Override
+            public Iterator<Edge> queryEdges(Query query) {
+                return edges;
+            }
+        };
+    }
+
+    private static final class CountCloseableIterator<T>
+            implements CloseableIterator<T> {
+
+        private final boolean fail;
+        private boolean consumed;
+        private boolean closed;
+
+        private CountCloseableIterator(boolean fail) {
+            this.fail = fail;
+        }
+
+        @Override
+        public boolean hasNext() {
+            if (this.fail) {
+                throw new IllegalStateException("Injected iterator failure");
+            }
+            return !this.consumed;
+        }
+
+        @Override
+        public T next() {
+            if (!this.hasNext()) {
+                throw new NoSuchElementException();
+            }
+            this.consumed = true;
+            return null;
+        }
+
+        @Override
+        public void close() {
+            this.closed = true;
+        }
+
+        public boolean closed() {
+            return this.closed;
+        }
+    }
+
+    @Test
+    public void testEdgeRangeCountRejectsUncommittedAddition() {
+        this.initSchema();
+        graph().schema().indexLabel("personByName")
+               .onV("person").by("name").create();
+        this.initGraph();
+        Vertex josh = graph().traversal().V()
+                             .hasLabel("person")
+                             .has("name", "josh")
+                             .next();
+        Vertex marko = graph().traversal().V()
+                              .hasLabel("person")
+                              .has("name", "marko")
+                              .next();
+        josh.addEdge("knows", marko);
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().E().range(1L, 3L).count());
+    }
+
+    @Test
+    public void testEdgeLimitCountRejectsUncommittedDeletion() {
+        this.initSchema();
+        this.initGraph();
+        Edge edge = graph().traversal().E().hasLabel("knows").next();
+        edge.remove();
+
+        assertUncommittedRangeUnsupported(
+                graph().traversal().E().limit(1L).count());
+    }
+
     @Test
     public void testRepeatAfterTextRangeFilterWithEmptyResult() {
         this.initTextRangeSchema(true);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
index c79db5056..38749ce7b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
@@ -17,7 +17,11 @@
 
 package org.apache.hugegraph.traversal.optimize;
 
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 import org.apache.hugegraph.HugeGraph;
@@ -29,10 +33,12 @@ import org.apache.hugegraph.testutil.Assert;
 import org.apache.hugegraph.type.define.DataType;
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
 import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
@@ -122,6 +128,31 @@ public class TraversalUtilOptimizeTest {
         Assert.assertTrue(hasStepExists(traversal));
     }
 
+    @Test
+    public void testExtractHasContainerKeepsMixedTextGraphHasStepLocal() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        PropertyKey age = propertyKey(1L, "age", DataType.INT);
+        PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+        Mockito.when(graph.propertyKey("age")).thenReturn(age);
+        Mockito.when(graph.propertyKey("name")).thenReturn(name);
+
+        Traversal.Admin<?, ?> traversal = traversal(
+                __.V().has("person", "name", TextP.containing("ar")),
+                graph);
+        HasStep<?> hasStep = (HasStep<?>) traversal.getEndStep();
+        hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+        HugeGraphStep<?, ?> newStep = replaceGraphStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertFalse(hasContainer(newStep, T.label.getAccessor()));
+        Assert.assertFalse(hasContainer(newStep, "age"));
+        Assert.assertFalse(hasContainer(newStep, "name"));
+        Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor()));
+        Assert.assertTrue(hasStepExists(traversal, "age"));
+        Assert.assertTrue(hasStepExists(traversal, "name"));
+    }
+
     @Test
     public void testExtractHasContainerKeepsTextRangeWithoutGraph() {
         Traversal.Admin<?, ?> traversal = __.V()
@@ -280,6 +311,31 @@ public class TraversalUtilOptimizeTest {
         Assert.assertTrue(hasStepExists(traversal));
     }
 
+    @Test
+    public void testExtractHasContainerKeepsMixedTextVertexHasStepLocal() {
+        HugeGraph graph = Mockito.mock(HugeGraph.class);
+        PropertyKey age = propertyKey(1L, "age", DataType.INT);
+        PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+        Mockito.when(graph.propertyKey("age")).thenReturn(age);
+        Mockito.when(graph.propertyKey("name")).thenReturn(name);
+
+        Traversal.Admin<?, ?> traversal = traversal(
+                __.V().out().has("person", "name", TextP.containing("ar")),
+                graph);
+        HasStep<?> hasStep = (HasStep<?>) traversal.getEndStep();
+        hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+        HugeVertexStep<?> newStep = replaceVertexStep(traversal);
+
+        TraversalUtil.extractHasContainer(newStep, traversal);
+
+        Assert.assertFalse(hasContainer(newStep, T.label.getAccessor()));
+        Assert.assertFalse(hasContainer(newStep, "age"));
+        Assert.assertFalse(hasContainer(newStep, "name"));
+        Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor()));
+        Assert.assertTrue(hasStepExists(traversal, "age"));
+        Assert.assertTrue(hasStepExists(traversal, "name"));
+    }
+
     @Test
     public void testExtractHasContainerRemovesSafeVertexHasStep() {
         HugeGraph graph = Mockito.mock(HugeGraph.class);
@@ -296,6 +352,40 @@ public class TraversalUtilOptimizeTest {
         Assert.assertFalse(hasStepExists(traversal));
     }
 
+    @Test
+    public void testLabelContainersWithImmutableList() {
+        assertNullHostileLabelCollection(List.of("person"));
+    }
+
+    @Test
+    public void testLabelContainersWithImmutableSet() {
+        assertNullHostileLabelCollection(Set.of("person"));
+    }
+
+    private static void assertNullHostileLabelCollection(Collection<String> labels) {
+        HasContainer within = new HasContainer(T.label.getAccessor(), P.within(labels));
+        Assert.assertTrue(TraversalUtil.canExtractHasContainer(null, within));
+        Assert.assertTrue(TraversalUtil.isPositiveLabelContainer(within));
+        HasContainer without = new HasContainer(T.label.getAccessor(), P.without(labels));
+        Assert.assertTrue(TraversalUtil.canExtractHasContainer(null, without));
+        Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(without));
+    }
+
+    @Test
+    public void testLabelContainersWithMutableNullValuesStayLocal() {
+        Collection<String> list = new ArrayList<>(List.of("person"));
+        list.add(null);
+        Collection<String> set = new HashSet<>(list);
+        for (Collection<String> labels : List.of(list, set)) {
+            HasContainer within = new HasContainer(T.label.getAccessor(), P.within(labels));
+            Assert.assertFalse(TraversalUtil.canExtractHasContainer(null, within));
+            Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(within));
+            HasContainer without = new HasContainer(T.label.getAccessor(), P.without(labels));
+            Assert.assertFalse(TraversalUtil.canExtractHasContainer(null, without));
+            Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(without));
+        }
+    }
+
     @Test
     public void testIsPositiveLabelContainer() {
         Assert.assertTrue(TraversalUtil.isPositiveLabelContainer(
@@ -314,6 +404,11 @@ public class TraversalUtilOptimizeTest {
         Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
                 new HasContainer(T.label.getAccessor(),
                                  P.within(Collections.emptyList()))));
+        Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
+                new HasContainer(T.label.getAccessor(), P.eq(null))));
+        Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
+                new HasContainer(T.label.getAccessor(),
+                                 P.within(null, "person"))));
     }
 
     @Test
@@ -419,7 +514,8 @@ public class TraversalUtilOptimizeTest {
         TraversalHelper.replaceStep((Step) origin, (Step) newStep, traversal);
     }
 
-    private static boolean hasContainer(HugeGraphStep<?, ?> step, String key) {
+    private static boolean hasContainer(HasContainerHolder step,
+                                        String key) {
         for (HasContainer has : step.getHasContainers()) {
             if (key.equals(has.getKey())) {
                 return true;