diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 0f9d34139f2..9d0d58844a9 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 00377dc99f2..484e2a7e0f0 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.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.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 @@ * @example
  * __.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
  * 
*/ public final class CountStrategy extends AbstractTraversalStrategy 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 RANGE_PREDICATES = new HashMap() {{ put(Contains.within, 1L); - put(Contains.without, 0L); + put(Contains.without, 1L); }}; private static final Set 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 void apply(final Traversal.Admin traversal) { } } } 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 void apply(final Traversal.Admin traversal) { } } + /** + * 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

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 @@ private boolean doStrategy(final Step step) { 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 77e02634289..63ff1aad94a 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 static Iterable generateTestParameters() { {__.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 static Iterable generateTestParameters() { {__.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 032c7e29f50..fd6134729e1 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 static Iterable generateTestParameters() { {__.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 e911fb72996..e3c7f0ca865 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 void shouldApplyStrategiesCorrectly() { 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 26d7bfdc7e3..299660d46c3 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 @@ private static IDictionary, 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, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(0))}}, {"g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX", new List, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(CountStrategy)).V().Where(__.OutE().Count().Is(0))}}, + {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX", new List, 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, 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, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().OutE().Count().Is(P.Gt(2))}}, + {"g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX", new List, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Map(__.OutE().Count().Is(P.Gt(1)))}}, + {"g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name", new List, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Filter(__.OutE().Count().Is(P.Gt(1).And(P.Lt(__.Constant(3l))))).Values("name")}}, + {"g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX", new List, ITraversal>> {(g,p) =>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", new List, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Union(__.Identity(), __.Has("name", "marko")).Count().Is(P.Without(2, 6, 4))}}, + {"g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX", new List, 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, ITraversal>> {(g,p) =>g.WithStrategies(new EarlyLimitStrategy()).V().Out().Order().By("name").ValueMap().Limit(3).Select("name")}}, {"g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX", new List, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(EarlyLimitStrategy)).V().Out().Order().By("name").ValueMap().Limit(3).Select("name")}}, {"g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V", new List, ITraversal>> {(g,p) =>g.WithStrategies(new EdgeLabelVerificationStrategy(throwException: true, logWarning: false)).V().Out()}}, @@ -3185,6 +3193,14 @@ private static IDictionary, 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, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Where(__.OutE().Count().Is(0))}}, {"g_withoutStrategiesXCountStrategyX_V_whereXoutE_count_isX0XX", new List, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(CountStrategy)).V().Where(__.OutE().Count().Is(0))}}, + {"g_withStrategiesXCountStrategyX_V_whereXoutE_count_isXgtX1XX_isXltX3XXX", new List, 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, 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, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().OutE().Count().Is(P.Gt(2))}}, + {"g_withStrategiesXCountStrategyX_V_mapXoutE_count_isXgtX1XXX", new List, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Map(__.OutE().Count().Is(P.Gt(1)))}}, + {"g_withStrategiesXCountStrategyX_V_filterXoutE_count_isXgtX1X_andXltXconstantX3XXXXX_name", new List, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Filter(__.OutE().Count().Is(P.Gt(1).And(P.Lt(__.Constant(3l))))).Values("name")}}, + {"g_withStrategiesXCountStrategyX_V_outE_count_asXaX_isXgtX1X_andXlteXselectXaXXXX", new List, ITraversal>> {(g,p) =>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", new List, ITraversal>> {(g,p) =>g.WithStrategies(new CountStrategy()).V().Union(__.Identity(), __.Has("name", "marko")).Count().Is(P.Without(2, 6, 4))}}, + {"g_withStrategiesXCountStrategyX_V_count_isXnotXlteX2XXX", new List, 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, ITraversal>> {(g,p) =>g.WithStrategies(new EarlyLimitStrategy()).V().Out().Order().By("name").ValueMap().Limit(3).Select("name")}}, {"g_withoutStrategiesXEarlyLimitStrategyX_V_out_order_byXnameX_valueMap_limitX3X_selectXnameX", new List, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(EarlyLimitStrategy)).V().Out().Order().By("name").ValueMap().Limit(3).Select("name")}}, {"g_withStrategiesXEdgeLabelVerificationStrategyXthrowException_true_logWarning_falseXX_V", new List, 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 023693f603f..15dc7f4fe62 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 961dd41f264..1f1d23a691d 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 3a00f1ddda9..dc5cb815185 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 @@ '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 c20718dabe8..ea245920b44 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(__.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(3l))))).Values(\"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(\"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(__.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 7df1f3fc2ca..fb268a70ca8 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 |