You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by co...@apache.org on 2026/10/22 07:21:16 UTC

(tinkerpop) branch master updated: Fix CountStrategy returning capped counts when the count is observed (#3717)

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

Cole-Greer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new fcba5860e2 Fix CountStrategy returning capped counts when the count is observed (#3717)
fcba5860e2 is described below

commit fcba5860e21836254a2c8f0761a39a7b5472140c
Author: Prashant Upadhyay <pr...@amazon.com>
AuthorDate: Thu Oct 22 00:21:09 2026 -0700

    Fix CountStrategy returning capped counts when the count is observed (#3717)
    
    CountStrategy no longer adds a limit() before count().is(P) when the
    capped count could pass P and then be read, e.g. out().count().is(gt(2))
    returned 3 instead of 6. The limit is still added when the is() chain
    rejects the cap or the count is only used as a filter. Predicates that
    contain a traversal are left unchanged, and the limits for without()
    and not(lte()) were one too small.
    
    Assisted-by: Kiro:claude-opus-5.5
---
 CHANGELOG.asciidoc                                 |   1 +
 .../strategy/optimization/CountStrategy.java       | 127 ++++++++++++++++++-
 .../strategy/optimization/CountStrategyTest.java   |  76 +++++++++++-
 .../optimization/LazyBarrierStrategyTest.java      |   3 +-
 .../traversal/util/TraversalExplanationTest.java   |   4 +-
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |  16 +++
 gremlin-go/driver/cucumber/gremlin.go              |   8 ++
 .../gremlin-javascript/test/cucumber/gremlin.js    |   8 ++
 .../src/main/python/tests/feature/gremlin.py       |   8 ++
 .../gremlin/language/translator/translations.json  | 136 +++++++++++++++++++++
 .../test/features/integrated/CountStrategy.feature |  95 ++++++++++++++
 11 files changed, 469 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index f5a83fccd9..41e5a47ac8 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -32,6 +32,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Fixed a `NullPointerException` when rolling back a transaction on a `TinkerStorageGraph` that has a vertex index but no edge index.
 * Added a pluggable storage layer to `TinkerStorageGraph` that durably persists each committed transaction to disk.
 * Removed automatic persistence from `TinkerMemoryGraph`, which is now purely in-memory and ignores `gremlin.tinkergraph.graphLocation`/`graphFormat`. Use `TinkerStorageGraph` for durability or `g.io()` for interchange.
+* Fixed `CountStrategy` so that it no longer adds a `limit()` before `count().is(P)` when the capped count can pass the predicate and is then observed downstream, e.g. `out().count().is(gt(2))`.
 * Removed the per-request `userAgent` option from all drivers so the user agent is sent only as the `User-Agent` header controlled by `enableUserAgentOnConnect`.
 
 [[release-4-0-0-beta-3]]
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategy.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategy.java
index 00377dc99f..484e2a7e0f 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategy.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategy.java
@@ -34,11 +34,13 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NotStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep;
 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.MatchStep;
 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.sideEffect.TraversalSideEffectStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStepContract;
 import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStepContract;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep;
@@ -46,9 +48,11 @@ import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversal
 import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -64,17 +68,20 @@ import java.util.function.BiPredicate;
  * @example <pre>
  * __.outE().count().is(0)      // is replaced by __.not(outE())
  * __.outE().count().is(lt(3))  // is replaced by __.outE().limit(3).count().is(lt(3))
- * __.outE().count().is(gt(3))  // is replaced by __.outE().limit(4).count().is(gt(3))
+ * __.where(outE().count().is(gt(3)))  // is replaced by __.where(outE().limit(4).count().is(gt(3)))
+ * __.outE().count().is(gt(3))  // is not changed as the real count is returned when it is greater than 3
  * </pre>
  */
 public final class CountStrategy extends AbstractTraversalStrategy<TraversalStrategy.OptimizationStrategy> implements TraversalStrategy.OptimizationStrategy {
 
+    // the limit for each predicate must be a count from which the predicate gives the same result for every larger
+    // count, so that capping the count at the limit can't change the outcome
     private static final Map<BiPredicate, Long> RANGE_PREDICATES = new HashMap<BiPredicate, Long>() {{
         put(Contains.within, 1L);
-        put(Contains.without, 0L);
+        put(Contains.without, 1L);
     }};
     private static final Set<PBiPredicate> INCREASED_OFFSET_SCALAR_PREDICATES =
-            Set.of(Compare.eq, Compare.neq, Compare.lte, Compare.gt, new NotP.NotPBiPredicate(Compare.eq), new NotP.NotPBiPredicate(Compare.neq), new NotP.NotPBiPredicate(Compare.lt), new NotP.NotPBiPredicate(Compare.gt));
+            Set.of(Compare.eq, Compare.neq, Compare.lte, Compare.gt, new NotP.NotPBiPredicate(Compare.eq), new NotP.NotPBiPredicate(Compare.neq), new NotP.NotPBiPredicate(Compare.lt), new NotP.NotPBiPredicate(Compare.gt), new NotP.NotPBiPredicate(Compare.lte));
 
     private static final CountStrategy INSTANCE = new CountStrategy();
 
@@ -196,7 +203,13 @@ public final class CountStrategy extends AbstractTraversalStrategy<TraversalStra
                             }
                         }
                     } else {
-                        TraversalHelper.insertBeforeStep(new RangeGlobalStep<>(traversal, 0L, highRange < 0 ? 0 : highRange), curr, traversal);
+                        final long limit = highRange < 0 ? 0 : highRange;
+                        final Long chainLimit;
+                        if (isLimitSafe(traversal, isStep, isStepPredicate, limit)) {
+                            TraversalHelper.insertBeforeStep(new RangeGlobalStep<>(traversal, 0L, limit), curr, traversal);
+                        } else if ((chainLimit = getIsChainLimit(traversal, isStep, limit)) != null) {
+                            TraversalHelper.insertBeforeStep(new RangeGlobalStep<>(traversal, 0L, chainLimit), curr, traversal);
+                        }
                     }
                     i++;
                 }
@@ -205,6 +218,107 @@ public final class CountStrategy extends AbstractTraversalStrategy<TraversalStra
         }
     }
 
+    /**
+     * Determines if a {@code limit(n)} can be placed in front of {@code count().is(predicate)} without changing the
+     * result. The limit caps the count at {@code n}, which is enough for the {@link IsStep} to decide, but if the
+     * predicate accepts {@code n} then the capped value passes through in place of the real count. That is only
+     * acceptable when nothing downstream observes the count value (e.g. a trailing {@code is()} in
+     * {@code count().is(gt(1)).is(lt(3))}, the root traversal or a {@code map()} child) and the child is only used as
+     * a filter.
+     */
+    private static boolean isLimitSafe(final Traversal.Admin<?, ?> traversal, final IsStepContract isStep,
+                                       final P isStepPredicate, final long limit) {
+        // if the capped count cannot pass the predicate, any count that does pass is exact
+        return !isStepPredicate.test(limit) || !isCountValueObserved(traversal, isStep);
+    }
+
+    /**
+     * Determines if the value emitted by {@code count().is(predicate)} is visible beyond a simple filter decision.
+     */
+    private static boolean isCountValueObserved(final Traversal.Admin<?, ?> traversal, final IsStepContract isStep) {
+        // a later step can access the count value. this includes the WhereEndStep that where() appends when its
+        // child ends with a label, as that compares the value to the path. labels elsewhere in a filter child can't
+        // be read from outside of it, so they don't matter here.
+        if (!(isStep.getNextStep() instanceof EmptyStep))
+            return true;
+
+        // the repeat() body always ends with a RepeatEndStep so it was already handled above, which leaves until()
+        // and emit() where the result is only used as a condition
+        if (traversal.getParent() instanceof RepeatStep) return false;
+
+        final TraversalParent parent = traversal.getParent();
+        // only parents that use the child traversal purely to test for the existence of a result. other FilterStep
+        // or SideEffectStep parents (e.g. dedup().by() or aggregate().by()) consume the value itself.
+        return !(parent instanceof TraversalFilterStep || parent instanceof NotStep
+                || parent instanceof ConnectiveStep || parent instanceof TraversalSideEffectStep);
+    }
+
+    /**
+     * Determines a limit for {@code count().is(a).is(b)...} that takes every {@code is()} in the chain into account.
+     * Each predicate gives the same result for every count of its limit or more, so with the largest of those limits
+     * the capped count passes the whole chain exactly when the real count does. If the chain then rejects the cap, any
+     * count that passes is exact; otherwise the limit is only safe when the count value is not observed afterwards.
+     *
+     * @return the limit to use or {@code null} if the chain can't be limited safely
+     */
+    private static Long getIsChainLimit(final Traversal.Admin<?, ?> traversal, final IsStepContract firstIsStep,
+                                        final long firstLimit) {
+        final List<P> predicates = new ArrayList<>();
+        predicates.add(firstIsStep.getPredicate());
+        final Long firstHighRange = getHighRange(firstIsStep.getPredicate());
+        if (firstHighRange == null) return null;
+        long limit = Math.max(firstLimit, firstHighRange);
+        IsStepContract lastIsStep = firstIsStep;
+        while (lastIsStep.getNextStep() instanceof IsStepContract) {
+            lastIsStep = (IsStepContract) lastIsStep.getNextStep();
+            final P predicate = lastIsStep.getPredicate();
+            if (predicate.hasTraversal()) return null;
+            final Long candidate = getHighRange(predicate);
+            if (candidate == null) return null;
+            limit = Math.max(limit, candidate);
+            predicates.add(predicate);
+        }
+
+        if (predicates.size() == 1) return null;
+
+        final long cap = limit;
+        final boolean capPasses = predicates.stream().allMatch(p -> p.test(cap));
+        return !capPasses || !isCountValueObserved(traversal, lastIsStep) ? cap : null;
+    }
+
+    /**
+     * Gets the smallest count from which {@code predicate} gives the same result for every larger count, or
+     * {@code null} if that can't be determined.
+     */
+    private static Long getHighRange(final P<?> predicate) {
+        try {
+            if (predicate instanceof ConnectiveP) {
+                long highRange = 0L;
+                for (final P<?> p : ((ConnectiveP<?>) predicate).getPredicates()) {
+                    final Long candidate = getHighRange(p);
+                    if (candidate == null) return null;
+                    highRange = Math.max(highRange, candidate);
+                }
+                return highRange;
+            }
+
+            final Object value = predicate.getValue();
+            final BiPredicate biPredicate = predicate.getBiPredicate();
+            if (value instanceof Number) {
+                final long offset = INCREASED_OFFSET_SCALAR_PREDICATES.contains(biPredicate) ? 1L : 0L;
+                return Math.max(0L, (long) Math.ceil(((Number) value).doubleValue()) + offset);
+            }
+            final Long offset = RANGE_PREDICATES.get(biPredicate);
+            if (value instanceof Collection && !((Collection<?>) value).isEmpty() && offset != null) {
+                final Object high = Collections.max((Collection) value);
+                if (high instanceof Number) return Math.max(0L, ((Number) high).longValue() + offset);
+            }
+        } catch (final ClassCastException ex) {
+            // mixed value types in within()/without() can't be compared, so the chain can't be reasoned about
+        }
+        return null;
+    }
+
     /**
      * The {@code traversal} argument was marked as one that could be converted to {@code not()}, so wrap it inside
      * that step and copy the labels.
@@ -227,6 +341,9 @@ public final class CountStrategy extends AbstractTraversalStrategy<TraversalStra
         final Step parent = step.getTraversal().getParent().asStep();
         return (parent instanceof FilterStep || parent.getLabels().isEmpty()) && // if the parent is labeled, then the count matters
                 !(parent.getNextStep() instanceof MatchStep.MatchEndStep && // if this is in a pattern match, then don't do it.
-                        ((MatchStep.MatchEndStep) parent.getNextStep()).getMatchKey().isPresent());
+                        ((MatchStep.MatchEndStep) parent.getNextStep()).getMatchKey().isPresent()) &&
+                // a traversal in the predicate (e.g. is(lt(__.constant(3))) or is(lte(__.select("a")))) is only
+                // resolved at runtime, so no limit can be derived that is guaranteed not to change the result
+                !((IsStepContract<?>) step.getNextStep()).getPredicate().hasTraversal();
     }
 }
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java
index 77e0263428..63ff1aad94 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java
@@ -68,10 +68,14 @@ public class CountStrategyTest {
                     {__.count().is(1), __.limit(2).count().is(1)},
                     {__.count().is(-1), __.limit(0).count().is(-1)}, // -1 for RangeGlobalStep means no upper limit
                     {__.count().is(-2), __.limit(0).count().is(-2)},
+                    // the limit is kept at the root when the capped count can't pass the predicate: a count of 0 fails
+                    // is(-2023) and between() excludes its upper bound of 10, so any count that does pass is exact
                     {__.count().is(-2023), __.limit(0).count().is(-2023)},
                     {__.count().is(P.between(0, 10)), __.limit(10).count().is(P.between(0, 10))},
                     {__.count().is(P.between(5, 10)), __.limit(10).count().is(P.between(5, 10))},
-                    {__.count().is(P.gt(10).or(lt(5))), __.limit(11).count().is(P.gt(10).or(lt(5)))},
+                    // limit(11) would cap e.g. 20 at 11, which passes gt(10) and would be returned in place of 20
+                    {__.count().is(P.gt(10).or(lt(5))), __.count().is(P.gt(10).or(lt(5)))},
+                    {__.where(__.count().is(P.gt(10).or(lt(5)))), __.where(__.limit(11).count().is(P.gt(10).or(lt(5))))},
                     {__.out().count().is(0), __.out().limit(1).count().is(0)},
                     {__.outE().count().is(lt(1)), __.outE().limit(1).count().is(lt(1))},
                     {__.both().count().is(lte(0)), __.both().limit(1).count().is(lte(0))},
@@ -84,15 +88,75 @@ public class CountStrategyTest {
                     {__.filter(__.out().out().count().is(0)), __.not(__.out().out())},
                     {__.local(aggregate("x")).count().is(0).as("a"), __.local(aggregate("x")).limit(1).count().is(0).as("a")},
                     {__.out().count().as("a").is(0), __.out().limit(1).count().as("a").is(0)},
-                    {__.out().count().is(neq(4)), __.out().limit(5).count().is(neq(4))},
+                    {__.out().count().is(neq(4)), __.out().count().is(neq(4))}, // root: the count value is returned
+                    {__.where(__.out().count().is(neq(4))), __.where(__.out().limit(5).count().is(neq(4)))},
+                    {__.out().where(__.count().is(neq(4))), __.out().where(__.limit(5).count().is(neq(4)))},
                     {__.out().count().is(lte(3)), __.out().limit(4).count().is(lte(3))},
                     {__.out().count().is(lt(3)), __.out().limit(3).count().is(lt(3))},
-                    {__.out().count().is(gt(2)), __.out().limit(3).count().is(gt(2))},
-                    {__.out().count().is(gte(2)), __.out().limit(2).count().is(gte(2))},
+                    {__.out().count().is(gt(2)), __.out().count().is(gt(2))}, // root: the count value is returned
+                    {__.filter(__.out().count().is(gt(2))), __.filter(__.out().limit(3).count().is(gt(2)))},
+                    {__.out().count().is(gte(2)), __.out().count().is(gte(2))}, // root: the count value is returned
+                    {__.filter(__.out().count().is(gte(2))), __.filter(__.out().limit(2).count().is(gte(2)))},
                     {__.out().count().is(inside(2, 4)), __.out().limit(4).count().is(inside(2, 4))},
-                    {__.out().count().is(outside(2, 4)), __.out().limit(5).count().is(outside(2, 4))},
+                    {__.out().count().is(outside(2, 4)), __.out().count().is(outside(2, 4))}, // root: the count value is returned
+                    {__.where(__.out().count().is(outside(2, 4))), __.where(__.out().limit(5).count().is(outside(2, 4)))},
                     {__.out().count().is(within(2, 6, 4)), __.out().limit(7).count().is(within(2, 6, 4))},
-                    {__.out().count().is(without(2, 6, 4)), __.out().limit(6).count().is(without(2, 6, 4))},
+                    {__.out().count().is(without(2, 6, 4)), __.out().count().is(without(2, 6, 4))}, // root: limit(7) passes and is returned
+                    // without() only gives the same result for every count above its largest value, so the limit is max + 1
+                    {__.where(__.out().count().is(without(2, 6, 4))), __.where(__.out().limit(7).count().is(without(2, 6, 4)))},
+                    // not(lte(2)) is gt(2), which needs a limit of 3 like gt(2) does
+                    {__.out().count().is(P.not(lte(2))), __.out().count().is(P.not(lte(2)))},
+                    {__.where(__.out().count().is(P.not(lte(2)))), __.where(__.out().limit(3).count().is(P.not(lte(2))))},
+                    // a chain of is() steps is limited by the largest limit of its predicates. if the chain rejects that
+                    // cap, any count that passes is exact
+                    {__.where(__.out().count().is(gt(1)).is(lt(3))), __.where(__.out().limit(3).count().is(gt(1)).is(lt(3)))},
+                    {__.where(__.out().count().is(gte(2)).is(lte(2))), __.where(__.out().limit(3).count().is(gte(2)).is(lte(2)))},
+                    {__.filter(__.out().count().is(neq(1)).is(lt(5))), __.filter(__.out().limit(5).count().is(neq(1)).is(lt(5)))},
+                    {__.out().count().is(gt(1)).is(lt(3)), __.out().limit(3).count().is(gt(1)).is(lt(3))},
+                    {__.map(__.out().count().is(gt(1)).is(lt(3))), __.map(__.out().limit(3).count().is(gt(1)).is(lt(3)))},
+                    // if the chain accepts the cap, it can only be limited when the count isn't observed afterwards
+                    {__.where(__.out().count().is(gt(1)).is(neq(2))), __.where(__.out().limit(3).count().is(gt(1)).is(neq(2)))},
+                    {__.out().count().is(gt(1)).is(neq(2)), __.out().count().is(gt(1)).is(neq(2))},
+                    {__.map(__.out().count().is(gt(1)).is(gt(2))), __.map(__.out().count().is(gt(1)).is(gt(2)))},
+                    {__.where(__.out().count().is(gt(1)).is(gt(2)).as("a")), __.where(__.out().count().is(gt(1)).is(gt(2)).as("a"))},
+                    // a traversal anywhere in the chain means the chain can't be reasoned about
+                    {__.where(__.out().count().is(gt(1)).is(lt(__.constant(3L)))), __.where(__.out().count().is(gt(1)).is(lt(__.constant(3L))))},
+                    // a capped count that passes is() must not be observed by later steps or the parent
+                    // a label at the end of the where() child turns it into a WhereTraversalStep, which compares the
+                    // count to the path, so the capped value would be observed
+                    {__.where(__.out().count().is(gt(1)).as("a")), __.where(__.out().count().is(gt(1)).as("a"))},
+                    // other labels in a filter-only child can't be read outside of it, so the limit is safe
+                    {__.where(__.out().count().as("a").is(gt(1))), __.where(__.out().limit(2).count().as("a").is(gt(1)))},
+                    {__.filter(__.out().count().as("a").is(gt(1))), __.filter(__.out().limit(2).count().as("a").is(gt(1)))},
+                    {__.filter(__.out().count().is(gt(1)).as("a")), __.filter(__.out().limit(2).count().is(gt(1)).as("a"))},
+                    {__.repeat(__.out()).until(__.out().count().is(gt(1)).is(lt(3))), __.repeat(__.out()).until(__.out().limit(3).count().is(gt(1)).is(lt(3)))},
+                    {__.map(__.out().count().is(gt(1))), __.map(__.out().count().is(gt(1)))},
+                    {__.flatMap(__.out().count().is(gte(2))), __.flatMap(__.out().count().is(gte(2)))},
+                    {__.local(__.out().count().is(gt(1))), __.local(__.out().count().is(gt(1)))},
+                    {__.project("a").by(__.out().count().is(gt(1))), __.project("a").by(__.out().count().is(gt(1)))},
+                    {__.dedup().by(__.out().count().is(gt(1))), __.dedup().by(__.out().count().is(gt(1)))},
+                    {__.aggregate("x").by(__.out().count().is(gt(1))), __.aggregate("x").by(__.out().count().is(gt(1)))},
+                    {__.repeat(__.out().count().is(gt(1))).times(1), __.repeat(__.out().count().is(gt(1))).times(1)},
+                    // traversal-valued predicates are resolved at runtime, so no safe limit can be derived, even in
+                    // a filter-only child and even when the traversal reads a label on count()
+                    {__.filter(__.out().count().is(gt(1).and(lt(__.constant(3L))))), __.filter(__.out().count().is(gt(1).and(lt(__.constant(3L)))))},
+                    {__.filter(__.out().count().as("a").is(gt(1).and(lte(__.select("a"))))), __.filter(__.out().count().as("a").is(gt(1).and(lte(__.select("a")))))},
+                    {__.out().count().is(gt(1).and(lte(__.constant(5L)))), __.out().count().is(gt(1).and(lte(__.constant(5L))))},
+                    {__.filter(__.out().count().is(lt(__.constant(1L)))), __.filter(__.out().count().is(lt(__.constant(1L))))},
+                    {__.filter(__.out().count().is(P.not(gt(__.constant(1L))))), __.filter(__.out().count().is(P.not(gt(__.constant(1L)))))},
+                    // still optimized: only exact counts below the limit can pass the first is()
+                    {__.where(__.out().count().is(lt(3)).is(gt(1))), __.where(__.out().limit(3).count().is(lt(3)).is(gt(1)))},
+                    {__.out().count().is(lt(3)).is(gt(1)), __.out().limit(3).count().is(lt(3)).is(gt(1))},
+                    {__.map(__.out().count().is(lte(3))), __.map(__.out().limit(4).count().is(lte(3)))},
+                    // still optimized: the parent only checks whether anything passed
+                    {__.where(__.out().count().is(gt(2))), __.where(__.out().limit(3).count().is(gt(2)))},
+                    {__.not(__.out().count().is(gte(2))), __.not(__.out().limit(2).count().is(gte(2)))},
+                    {__.sideEffect(__.out().count().is(gt(2))), __.sideEffect(__.out().limit(3).count().is(gt(2)))},
+                    {__.repeat(__.out()).until(__.out().count().is(gt(2))), __.repeat(__.out()).until(__.out().limit(3).count().is(gt(2)))},
+                    {__.repeat(__.out()).emit(__.out().count().is(gte(2))), __.repeat(__.out()).emit(__.out().limit(2).count().is(gte(2)))},
+                    {__.out().until(__.out().count().is(gt(2))).repeat(__.out()), __.out().until(__.out().limit(3).count().is(gt(2))).repeat(__.out())},
+                    {__.and(__.out().count().is(gt(1)), __.has("name")), __.and(__.out().limit(2).count().is(gt(1)), __.has("name"))},
+                    {__.or(__.out().count().is(gte(3)), __.has("name")), __.or(__.out().limit(3).count().is(gte(3)), __.has("name"))},
                     {__.map(__.count().is(0)), __.map(__.limit(1).count().is(0))},
                     {__.flatMap(__.count().is(0)), __.flatMap(__.limit(1).count().is(0))},
                     {__.flatMap(__.count().is(0)).as("a"), __.flatMap(__.count().is(0)).as("a")}, // no change
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/LazyBarrierStrategyTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/LazyBarrierStrategyTest.java
index 032c7e29f5..fd6134729e 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/LazyBarrierStrategyTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/LazyBarrierStrategyTest.java
@@ -78,7 +78,8 @@ public class LazyBarrierStrategyTest {
                 {__.out().out().out().count(), __.out().out().barrier(LAZY_SIZE).out().count(), Collections.emptyList()},
                 {__.out().out().out().out().count(), __.out().out().barrier(LAZY_SIZE).out().barrier(LAZY_SIZE).out().count(), Collections.emptyList()},
                 {__.out().out().out().count(), __.out().out().barrier(LAZY_SIZE).outE().count(), Arrays.asList(CountStrategy.instance(), AdjacentToIncidentStrategy.instance(), GValueReductionStrategy.instance())},
-                {__.out().out().out().count().is(P.gt(10)), __.out().out().barrier(LAZY_SIZE).outE().limit(11).count().is(P.gt(10)), Arrays.asList(CountStrategy.instance(), AdjacentToIncidentStrategy.instance(), GValueReductionStrategy.instance())},
+                {__.out().out().out().count().is(P.gt(10)), __.out().out().barrier(LAZY_SIZE).outE().count().is(P.gt(10)), Arrays.asList(CountStrategy.instance(), AdjacentToIncidentStrategy.instance(), GValueReductionStrategy.instance())},
+                {__.where(__.out().out().out().count().is(P.gt(10))), __.where(__.out().out().barrier(LAZY_SIZE).outE().limit(11).count().is(P.gt(10))), Arrays.asList(CountStrategy.instance(), AdjacentToIncidentStrategy.instance(), GValueReductionStrategy.instance())},
                 {__.outE().inV().outE().inV().outE().inV().groupCount(), __.outE().inV().outE().inV().barrier(LAZY_SIZE).outE().inV().groupCount(), Collections.emptyList()},
                 {__.outE().inV().outE().inV().outE().inV().groupCount(), __.out().out().barrier(LAZY_SIZE).out().groupCount(), List.of(IncidentToAdjacentStrategy.instance(), GValueReductionStrategy.instance())},
                 {__.out().out().has("age", 32).out().count(), __.out().out().barrier(LAZY_SIZE).has("age", 32).out().count(), Collections.emptyList()},
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/TraversalExplanationTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/TraversalExplanationTest.java
index e911fb7299..e3c7f0ca86 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/TraversalExplanationTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/util/TraversalExplanationTest.java
@@ -108,7 +108,9 @@ public class TraversalExplanationTest {
         traversal.setStrategies(TraversalStrategies.GlobalCache.getStrategies(Graph.class).clone());
         checkTraversalExplanation(traversal.explain(), List.of(new ExplainExpectation(AdjacentToIncidentStrategy.class, "[VertexStepPlaceholder(IN,edge)")));
         ///
-        traversal = __.outE().inV().group().by(__.inE().outV().groupCount().by(__.both().count().is(P.gt(2)))).asAdmin();
+        // lt(3) so that CountStrategy can safely limit: the count is a by() key, so a predicate that accepts the cap
+        // (e.g. gt(2)) must be left unlimited
+        traversal = __.outE().inV().group().by(__.inE().outV().groupCount().by(__.both().count().is(P.lt(3)))).asAdmin();
         traversal.setStrategies(TraversalStrategies.GlobalCache.getStrategies(Graph.class).clone().removeStrategies(ProductiveByStrategy.class));
         checkTraversalExplanation(traversal.explain(), List.of(new ExplainExpectation(IncidentToAdjacentStrategy.class, "[VertexStepPlaceholder(IN,vertex)"),
                 new ExplainExpectation(IncidentToAdjacentStrategy.class, "[VertexStepPlaceholder(OUT,vertex)"),
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 5ae9b7af18..f658e91537 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -826,6 +826,14 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_withoutStrategiesXConnectiveStrategyX_V_hasXname_markoX_or_whereXinXknowsX_hasXname_markoXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(ConnectiveStrategy)).V().Has("name", "marko").Or().Where(__.In("knows").Has("name", "marko"))}}, 
                {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(0))}}, 
                {"g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(CountStrategy)).V().Where(__.OutE().Count().Is(0))}}, 
+               {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(P.Gt(1)).Is(P.Lt(3)))}}, 
+               {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(P.Lt(3)).Is(P.Gt(1)))}}, 
+               {"g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().OutE().Count().Is(P.Gt(2))}}, 
+               {"g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Map<object>(__.OutE().Count().Is(P.Gt(1)))}}, 
+               {"g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Filter(__.OutE().Count().Is(P.Gt(1).And(P.Lt(__.Constant<object>(3l))))).Values<object>("name")}}, 
+               {"g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().OutE().Count().As("a").Is(P.Gt(1).And(P.Lte(__.Select<object>("a"))))}}, 
+               {"g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Union<object>(__.Identity(), __.Has("name", "marko")).Count().Is(P.Without(2, 6, 4))}}, 
+               {"g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Count().Is(P.Not(P.Lte(2)))}}, 
                {"g_withStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new EarlyLimitStrategy()).V().Out().Order().By("name").ValueMap<object, object>().Limit<object>(3).Select<object>("name")}}, 
                {"g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(EarlyLimitStrategy)).V().Out().Order().By("name").ValueMap<object, object>().Limit<object>(3).Select<object>("name")}}, 
                {"g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new EdgeLabelVerificationStrategy(throwException: true, logWarning: false)).V().Out()}}, 
@@ -3193,6 +3201,14 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_withoutStrategiesXConnectiveStrategyX_V_hasXname_markoX_or_whereXinXknowsX_hasXname_markoXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(ConnectiveStrategy)).V().Has("name", "marko").Or().Where(__.In("knows").Has("name", "marko"))}}, 
                {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(0))}}, 
                {"g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(CountStrategy)).V().Where(__.OutE().Count().Is(0))}}, 
+               {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(P.Gt(1)).Is(P.Lt(3)))}}, 
+               {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(P.Lt(3)).Is(P.Gt(1)))}}, 
+               {"g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().OutE().Count().Is(P.Gt(2))}}, 
+               {"g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Map<object>(__.OutE().Count().Is(P.Gt(1)))}}, 
+               {"g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Filter(__.OutE().Count().Is(P.Gt(1).And(P.Lt(__.Constant<object>(3l))))).Values<object>("name")}}, 
+               {"g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().OutE().Count().As("a").Is(P.Gt(1).And(P.Lte(__.Select<object>("a"))))}}, 
+               {"g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Union<object>(__.Identity(), __.Has("name", "marko")).Count().Is(P.Without(2, 6, 4))}}, 
+               {"g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Count().Is(P.Not(P.Lte(2)))}}, 
                {"g_withStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new EarlyLimitStrategy()).V().Out().Order().By("name").ValueMap<object, object>().Limit<object>(3).Select<object>("name")}}, 
                {"g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(EarlyLimitStrategy)).V().Out().Order().By("name").ValueMap<object, object>().Limit<object>(3).Select<object>("name")}}, 
                {"g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.WithStrategies(new EdgeLabelVerificationStrategy(throwException: true, logWarning: false)).V().Out()}}, 
diff --git a/gremlin-go/driver/cucumber/gremlin.go b/gremlin-go/driver/cucumber/gremlin.go
index 3239554577..659da8b5c4 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -779,6 +779,14 @@ var translationMap = map[string][]func(g *gremlingo.GraphTraversalSource, p map[
     "g_withoutStrategiesXConnectiveStrategyX_V_hasXname_markoX_or_whereXinXknowsX_hasXname_markoXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithoutStrategies(gremlingo.ConnectiveStrategy()).V().Has("name", "marko").Or().Where(gremlingo.T__.In("knows").Has("name", "marko"))}}, 
     "g_withStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Where(gremlingo.T__.OutE().Count().Is(0))}}, 
     "g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithoutStrategies(gremlingo.CountStrategy()).V().Where(gremlingo.T__.OutE().Count().Is(0))}}, 
+    "g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Where(gremlingo.T__.OutE().Count().Is(gremlingo.P.Gt(1)).Is(gremlingo.P.Lt(3)))}}, 
+    "g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Where(gremlingo.T__.OutE().Count().Is(gremlingo.P.Lt(3)).Is(gremlingo.P.Gt(1)))}}, 
+    "g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().OutE().Count().Is(gremlingo.P.Gt(2))}}, 
+    "g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Map(gremlingo.T__.OutE().Count().Is(gremlingo.P.Gt(1)))}}, 
+    "g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Filter(gremlingo.T__.OutE().Count().Is(gremlingo.P.Gt(1).And(gremlingo.P.Lt(gremlingo.T__.Constant(int64(3)))))).Values("name")}}, 
+    "g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().OutE().Count().As("a").Is(gremlingo.P.Gt(1).And(gremlingo.P.Lte(gremlingo.T__.Select("a"))))}}, 
+    "g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Union(gremlingo.T__.Identity(), gremlingo.T__.Has("name", "marko")).Count().Is(gremlingo.P.Without(2, 6, 4))}}, 
+    "g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.CountStrategy()).V().Count().Is(gremlingo.P.Not(gremlingo.P.Lte(2)))}}, 
     "g_withStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.EarlyLimitStrategy()).V().Out().Order().By("name").ValueMap().Limit(3).Select("name")}}, 
     "g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithoutStrategies(gremlingo.EarlyLimitStrategy()).V().Out().Order().By("name").ValueMap().Limit(3).Select("name")}}, 
     "g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V": {func(g *gremlingo.GraphTraversalSource, p map[string]interface{}) *gremlingo.GraphTraversal {return g.WithStrategies(gremlingo.EdgeLabelVerificationStrategy(gremlingo.EdgeLabelVerificationStrategyConfig{ThrowException: true, LogWarning: false})).V().Out()}}, 
diff --git a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
index 302dc69056..ec836756d3 100644
--- a/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-js/gremlin-javascript/test/cucumber/gremlin.js
@@ -810,6 +810,14 @@ const gremlins = {
     g_withoutStrategiesXConnectiveStrategyX_V_hasXname_markoX_or_whereXinXknowsX_hasXname_markoXX: [function({g}) { return g.withoutStrategies(ConnectiveStrategy).V().has("name", "marko").or().where(__.in_("knows").has("name", "marko")) }], 
     g_withStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX: [function({g}) { return g.withStrategies(new CountStrategy()).V().where(__.outE().count().is(0)) }], 
     g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX: [function({g}) { return g.withoutStrategies(CountStrategy).V().where(__.outE().count().is(0)) }], 
+    g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX: [function({g}) { return g.withStrategies(new CountStrategy()).V().where(__.outE().count().is(P.gt(1)).is(P.lt(3))) }], 
+    g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX: [function({g}) { return g.withStrategies(new CountStrategy()).V().where(__.outE().count().is(P.lt(3)).is(P.gt(1))) }], 
+    g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX: [function({g}) { return g.withStrategies(new CountStrategy()).V().outE().count().is(P.gt(2)) }], 
+    g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX: [function({g}) { return g.withStrategies(new CountStrategy()).V().map(__.outE().count().is(P.gt(1))) }], 
+    g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name: [function({g}) { return g.withStrategies(new CountStrategy()).V().filter(__.outE().count().is(P.gt(1).and(P.lt(__.constant(3))))).values("name") }], 
+    g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX: [function({g}) { return g.withStrategies(new CountStrategy()).V().outE().count().as("a").is(P.gt(1).and(P.lte(__.select("a")))) }], 
+    g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX: [function({g}) { return g.withStrategies(new CountStrategy()).V().union(__.identity(), __.has("name", "marko")).count().is(P.without(2, 6, 4)) }], 
+    g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX: [function({g}) { return g.withStrategies(new CountStrategy()).V().count().is(P.not(P.lte(2))) }], 
     g_withStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX: [function({g}) { return g.withStrategies(new EarlyLimitStrategy()).V().out().order().by("name").valueMap().limit(3).select("name") }], 
     g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX: [function({g}) { return g.withoutStrategies(EarlyLimitStrategy).V().out().order().by("name").valueMap().limit(3).select("name") }], 
     g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V: [function({g}) { return g.withStrategies(new EdgeLabelVerificationStrategy({throwException: true, logWarning: false})).V().out() }], 
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py b/gremlin-python/src/main/python/tests/feature/gremlin.py
index 1b93538588..9cb22d1ea4 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -784,6 +784,14 @@ world.gremlins = {
     'g_withoutStrategiesXConnectiveStrategyX_V_hasXname_markoX_or_whereXinXknowsX_hasXname_markoXX': [(lambda g:g.without_strategies(ConnectiveStrategy).V().has('name', 'marko').or_().where(__.in_('knows').has('name', 'marko')))], 
     'g_withStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX': [(lambda g:g.with_strategies(CountStrategy()).V().where(__.out_e().count().is_(0)))], 
     'g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX': [(lambda g:g.without_strategies(CountStrategy).V().where(__.out_e().count().is_(0)))], 
+    'g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX': [(lambda g:g.with_strategies(CountStrategy()).V().where(__.out_e().count().is_(P.gt(1)).is_(P.lt(3))))], 
+    'g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX': [(lambda g:g.with_strategies(CountStrategy()).V().where(__.out_e().count().is_(P.lt(3)).is_(P.gt(1))))], 
+    'g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX': [(lambda g:g.with_strategies(CountStrategy()).V().out_e().count().is_(P.gt(2)))], 
+    'g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX': [(lambda g:g.with_strategies(CountStrategy()).V().map(__.out_e().count().is_(P.gt(1))))], 
+    'g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name': [(lambda g:g.with_strategies(CountStrategy()).V().filter_(__.out_e().count().is_(P.gt(1).and_(P.lt(__.constant(long(3)))))).values('name'))], 
+    'g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX': [(lambda g:g.with_strategies(CountStrategy()).V().out_e().count().as_('a').is_(P.gt(1).and_(P.lte(__.select('a')))))], 
+    'g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX': [(lambda g:g.with_strategies(CountStrategy()).V().union(__.identity(), __.has('name', 'marko')).count().is_(P.without(2, 6, 4)))], 
+    'g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX': [(lambda g:g.with_strategies(CountStrategy()).V().count().is_(P.not_(P.lte(2))))], 
     'g_withStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX': [(lambda g:g.with_strategies(EarlyLimitStrategy()).V().out().order().by('name').value_map().limit(3).select('name'))], 
     'g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX': [(lambda g:g.without_strategies(EarlyLimitStrategy).V().out().order().by('name').value_map().limit(3).select('name'))], 
     'g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V': [(lambda g:g.with_strategies(EdgeLabelVerificationStrategy(throw_exception=True, log_warning=False)).V().out())], 
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
index b7ea665a50..b8531e3341 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/language/translator/translations.json
@@ -14000,6 +14000,142 @@
             }
         ]
     },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().where(outE().count().is(gt(1)).is(lt(3)))",
+                "language": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.gt(1)).is(P.lt(3)))",
+                "canonical": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.gt(1)).is(P.lt(3)))",
+                "anonymized": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.gt(number0)).is(P.lt(number1)))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(P.Gt(1)).Is(P.Lt(3)))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().Where(gremlingo.T__.OutE().Count().Is(gremlingo.P.Gt(1)).Is(gremlingo.P.Lt(3)))",
+                "groovy": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.gt(1)).is(P.lt(3)))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().where(__.outE().count().is(P.gt(1)).is(P.lt(3)))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().where(__.outE().count().is(P.gt(1)).is(P.lt(3)))",
+                "python": "g.with_strategies(CountStrategy()).V().where(__.out_e().count().is_(P.gt(1)).is_(P.lt(3)))"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().where(outE().count().is(lt(3)).is(gt(1)))",
+                "language": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.lt(3)).is(P.gt(1)))",
+                "canonical": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.lt(3)).is(P.gt(1)))",
+                "anonymized": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.lt(number0)).is(P.gt(number1)))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(P.Lt(3)).Is(P.Gt(1)))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().Where(gremlingo.T__.OutE().Count().Is(gremlingo.P.Lt(3)).Is(gremlingo.P.Gt(1)))",
+                "groovy": "g.withStrategies(CountStrategy).V().where(__.outE().count().is(P.lt(3)).is(P.gt(1)))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().where(__.outE().count().is(P.lt(3)).is(P.gt(1)))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().where(__.outE().count().is(P.lt(3)).is(P.gt(1)))",
+                "python": "g.with_strategies(CountStrategy()).V().where(__.out_e().count().is_(P.lt(3)).is_(P.gt(1)))"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().outE().count().is(gt(2))",
+                "language": "g.withStrategies(CountStrategy).V().outE().count().is(P.gt(2))",
+                "canonical": "g.withStrategies(CountStrategy).V().outE().count().is(P.gt(2))",
+                "anonymized": "g.withStrategies(CountStrategy).V().outE().count().is(P.gt(number0))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().OutE().Count().Is(P.Gt(2))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().OutE().Count().Is(gremlingo.P.Gt(2))",
+                "groovy": "g.withStrategies(CountStrategy).V().outE().count().is(P.gt(2))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().outE().count().is(P.gt(2))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().outE().count().is(P.gt(2))",
+                "python": "g.with_strategies(CountStrategy()).V().out_e().count().is_(P.gt(2))"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().map(outE().count().is(gt(1)))",
+                "language": "g.withStrategies(CountStrategy).V().map(__.outE().count().is(P.gt(1)))",
+                "canonical": "g.withStrategies(CountStrategy).V().map(__.outE().count().is(P.gt(1)))",
+                "anonymized": "g.withStrategies(CountStrategy).V().map(__.outE().count().is(P.gt(number0)))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().Map<object>(__.OutE().Count().Is(P.Gt(1)))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().Map(gremlingo.T__.OutE().Count().Is(gremlingo.P.Gt(1)))",
+                "groovy": "g.withStrategies(CountStrategy).V().map(__.outE().count().is(P.gt(1)))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().map(__.outE().count().is(P.gt(1)))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().map(__.outE().count().is(P.gt(1)))",
+                "python": "g.with_strategies(CountStrategy()).V().map(__.out_e().count().is_(P.gt(1)))"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().filter(outE().count().is(P.gt(1).and(P.lt(__.constant(3l))))).values(\"name\")",
+                "language": "g.withStrategies(CountStrategy).V().filter(__.outE().count().is(P.gt(1).and(P.lt(__.constant(3l))))).values(\"name\")",
+                "canonical": "g.withStrategies(CountStrategy).V().filter(__.outE().count().is(P.gt(1).and(P.lt(__.constant(3l))))).values(\"name\")",
+                "anonymized": "g.withStrategies(CountStrategy).V().filter(__.outE().count().is(P.gt(number0).and(P.lt(__.constant(long0))))).values(string0)",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().Filter(__.OutE().Count().Is(P.Gt(1).And(P.Lt(__.Constant<object>(3l))))).Values<object>(\"name\")",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().Filter(gremlingo.T__.OutE().Count().Is(gremlingo.P.Gt(1).And(gremlingo.P.Lt(gremlingo.T__.Constant(int64(3)))))).Values(\"name\")",
+                "groovy": "g.withStrategies(CountStrategy).V().filter(__.outE().count().is(P.gt(1).and(P.lt(__.constant(3l))))).values(\"name\")",
+                "java": "g.withStrategies(CountStrategy.instance()).V().filter(__.outE().count().is(P.gt(1).and(P.lt(__.constant(3l))))).values(\"name\")",
+                "javascript": "g.withStrategies(new CountStrategy()).V().filter(__.outE().count().is(P.gt(1).and(P.lt(__.constant(3))))).values(\"name\")",
+                "python": "g.with_strategies(CountStrategy()).V().filter_(__.out_e().count().is_(P.gt(1).and_(P.lt(__.constant(long(3)))))).values('name')"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().outE().count().as(\"a\").is(P.gt(1).and(P.lte(__.select(\"a\"))))",
+                "language": "g.withStrategies(CountStrategy).V().outE().count().as(\"a\").is(P.gt(1).and(P.lte(__.select(\"a\"))))",
+                "canonical": "g.withStrategies(CountStrategy).V().outE().count().as(\"a\").is(P.gt(1).and(P.lte(__.select(\"a\"))))",
+                "anonymized": "g.withStrategies(CountStrategy).V().outE().count().as(string0).is(P.gt(number0).and(P.lte(__.select(string0))))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().OutE().Count().As(\"a\").Is(P.Gt(1).And(P.Lte(__.Select<object>(\"a\"))))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().OutE().Count().As(\"a\").Is(gremlingo.P.Gt(1).And(gremlingo.P.Lte(gremlingo.T__.Select(\"a\"))))",
+                "groovy": "g.withStrategies(CountStrategy).V().outE().count().as(\"a\").is(P.gt(1).and(P.lte(__.select(\"a\"))))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().outE().count().as(\"a\").is(P.gt(1).and(P.lte(__.select(\"a\"))))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().outE().count().as(\"a\").is(P.gt(1).and(P.lte(__.select(\"a\"))))",
+                "python": "g.with_strategies(CountStrategy()).V().out_e().count().as_('a').is_(P.gt(1).and_(P.lte(__.select('a'))))"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().union(identity(), has(\"name\", \"marko\")).count().is(P.without(2, 6, 4))",
+                "language": "g.withStrategies(CountStrategy).V().union(__.identity(), __.has(\"name\", \"marko\")).count().is(P.without(2, 6, 4))",
+                "canonical": "g.withStrategies(CountStrategy).V().union(__.identity(), __.has(\"name\", \"marko\")).count().is(P.without(2, 6, 4))",
+                "anonymized": "g.withStrategies(CountStrategy).V().union(__.identity(), __.has(string0, string1)).count().is(P.without(number0, number1, number2))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().Union<object>(__.Identity(), __.Has(\"name\", \"marko\")).Count().Is(P.Without(2, 6, 4))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().Union(gremlingo.T__.Identity(), gremlingo.T__.Has(\"name\", \"marko\")).Count().Is(gremlingo.P.Without(2, 6, 4))",
+                "groovy": "g.withStrategies(CountStrategy).V().union(__.identity(), __.has(\"name\", \"marko\")).count().is(P.without(2, 6, 4))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().union(__.identity(), __.has(\"name\", \"marko\")).count().is(P.without(2, 6, 4))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().union(__.identity(), __.has(\"name\", \"marko\")).count().is(P.without(2, 6, 4))",
+                "python": "g.with_strategies(CountStrategy()).V().union(__.identity(), __.has('name', 'marko')).count().is_(P.without(2, 6, 4))"
+            }
+        ]
+    },
+    {
+        "scenario": "g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX",
+        "traversals": [
+            {
+                "original": "g.withStrategies(CountStrategy).V().count().is(P.not(P.lte(2)))",
+                "language": "g.withStrategies(CountStrategy).V().count().is(P.not(P.lte(2)))",
+                "canonical": "g.withStrategies(CountStrategy).V().count().is(P.not(P.lte(2)))",
+                "anonymized": "g.withStrategies(CountStrategy).V().count().is(P.not(P.lte(number0)))",
+                "dotnet": "g.WithStrategies(new CountStrategy()).V().Count().Is(P.Not(P.Lte(2)))",
+                "go": "g.WithStrategies(gremlingo.CountStrategy()).V().Count().Is(gremlingo.P.Not(gremlingo.P.Lte(2)))",
+                "groovy": "g.withStrategies(CountStrategy).V().count().is(P.not(P.lte(2)))",
+                "java": "g.withStrategies(CountStrategy.instance()).V().count().is(P.not(P.lte(2)))",
+                "javascript": "g.withStrategies(new CountStrategy()).V().count().is(P.not(P.lte(2)))",
+                "python": "g.with_strategies(CountStrategy()).V().count().is_(P.not_(P.lte(2)))"
+            }
+        ]
+    },
     {
         "scenario": "g_withStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX",
         "traversals": [
diff --git a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/integrated/CountStrategy.feature b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/integrated/CountStrategy.feature
index 7df1f3fc2c..fb268a70ca 100644
--- a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/integrated/CountStrategy.feature
+++ b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/integrated/CountStrategy.feature
@@ -44,3 +44,98 @@ Feature: Step - CountStrategy
       | v[vadas] |
       | v[lop] |
       | v[ripple]  |
+
+  # a following is() must see the real count, not the value capped by the limit() CountStrategy injects
+  Scenario: g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().where(outE().count().is(gt(1)).is(lt(3)))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | v[josh] |
+
+  # the first is() only passes counts below the limit, so the limit is safe and the count seen downstream is exact
+  Scenario: g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXltX3XX_isXgtX1XXX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().where(outE().count().is(lt(3)).is(gt(1)))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | v[josh] |
+
+  Scenario: g_withStrategiesXCountStrategyX_V_outE_count_isXgtX2XX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().outE().count().is(gt(2))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | d[6].l |
+
+  Scenario: g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().map(outE().count().is(gt(1)))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | d[3].l |
+      | d[2].l |
+
+  # the lt() bound is a traversal, so the strategy can't know it and must not cap the count
+  Scenario: g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().filter(outE().count().is(P.gt(1).and(P.lt(__.constant(3l))))).values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | josh |
+
+  # the predicate traversal reads the label on count(), so it must see the real count
+  Scenario: g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().outE().count().as("a").is(P.gt(1).and(P.lte(__.select("a"))))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | d[6].l |
+
+  # without() accepts every count above its largest value, so a count of 7 must not be capped at 6
+  Scenario: g_withStrategiesXCountStrategyX_V_unionXidentity_hasXname_markoXX_count_isXwithoutX2_6_4XX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().union(identity(), has("name", "marko")).count().is(P.without(2, 6, 4))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | d[7].l |
+
+  # not(lte(2)) is gt(2), so the count must not be capped at 2
+  Scenario: g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX
+    Given the modern graph
+    And the traversal of
+      """
+      g.withStrategies(CountStrategy).V().count().is(P.not(P.lte(2)))
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | d[6].l |