upadhyay-prashant commented on code in PR #3717:
URL: https://github.com/apache/tinkerpop/pull/3717#discussion_r4290151978


##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java:
##########
@@ -71,7 +71,7 @@ public static Iterable<Object[]> generateTestParameters() {
                     {__.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)))},
+                    {__.count().is(P.gt(10).or(lt(5))), 
__.count().is(P.gt(10).or(lt(5)))}, // capped count of 11 would pass and be 
returned

Review Comment:
   The difference is whether the capped count can pass the predicate. If it 
can't, any count that does pass is below the cap and therefore exact, so the 
limit is harmless even at the root:
   
   - `is(-2023)`: `limit(0)` gives a count of 0, which fails the predicate (and 
a count can never be negative anyway).
   - `between(0, 10)` / `between(5, 10)`: the upper bound is exclusive, so the 
cap of 10 fails.
   - `gt(10).or(lt(5))`: the cap of 11 passes `gt(10)`, so a traversal of 20 
elements would return 11. That's the case the PR fixes.
   
   Dropping the limit for every root-level case would be simpler, but it would 
throw away optimizations that are always safe. I've added a comment above these 
cases in the test to explain this.
   



##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java:
##########
@@ -84,15 +84,46 @@ public static Iterable<Object[]> 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))},

Review Comment:
   Agreed, and done. Every case whose expectation changed now has a counterpart 
that is still limited:
   
   - `out().count().is(neq(4))` -> `where(out().count().is(neq(4)))` and your 
`out().where(count().is(neq(4)))`, both `limit(5)`
   - `is(gt(2))` -> `filter(out().count().is(gt(2)))`, `limit(3)`
   - `is(gte(2))` -> `filter(out().count().is(gte(2)))`, `limit(2)`
   - `is(outside(2, 4))` -> `where(out().count().is(outside(2, 4)))`, `limit(5)`
   - `is(gt(10).or(lt(5)))` -> `where(count().is(gt(10).or(lt(5))))`, 
`limit(11)`
   - `LazyBarrierStrategyTest` root `is(gt(10))` -> a `where()` version that 
keeps `limit(11)`
   



##########
gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/strategy/optimization/CountStrategyTest.java:
##########
@@ -84,15 +84,46 @@ public static Iterable<Object[]> 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
                     {__.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
+                    {__.out().count().is(gte(2)), 
__.out().count().is(gte(2))}, // root: the count value is returned
                     {__.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
                     {__.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))},
+                    // a capped count that passes is() must not be observed by 
later steps or the parent
+                    {__.where(__.out().count().is(gt(1)).is(lt(3))), 
__.where(__.out().count().is(gt(1)).is(lt(3)))},
+                    {__.where(__.out().count().is(gte(2)).is(lte(2))), 
__.where(__.out().count().is(gte(2)).is(lte(2)))},
+                    {__.filter(__.out().count().is(neq(1)).is(lt(5))), 
__.filter(__.out().count().is(neq(1)).is(lt(5)))},
+                    {__.where(__.out().count().is(gt(1)).as("a")), 
__.where(__.out().count().is(gt(1)).as("a"))},
+                    {__.where(__.out().count().as("a").is(gt(1))), 
__.where(__.out().count().as("a").is(gt(1)))},

Review Comment:
   You're right. Treating labels as "observed" was overly conservative, and 
I've removed that check. In a filter-only child, labels can't be read from 
outside it, so `where(out().count().as("a").is(gt(1)))` and both `filter(...)` 
label cases now get the limit.
   
   The one exception is a label at the end of the `where()` child, e.g. 
`where(out().count().is(gt(1)).as("a"))`. That makes `where()` build a 
`WhereTraversalStep`, which appends a `WhereEndStep` that compares the count to 
the path value `"a"`, so the capped value would be observed. That case is still 
caught by the existing "a step follows `is()`" check, so no label-specific 
logic is needed.
   
   The cases that still must not be limited inside `where()`/`filter()` are the 
ones with a following `is()`, e.g. `where(out().count().is(gt(1)).is(lt(3)))`. 
The second `is()` sees the capped value. The new `CountStrategy.feature` 
scenarios show the wrong results this caused on the modern graph.
   
   Looking at where a label could actually be read from also turned up one more 
case: the `is()` predicate itself can hold a traversal 
(`P.gt(__.constant(29))`), and that traversal runs inside the child. For 
example, `filter(outE().count().is(gt(1).and(lt(__.constant(3)))))` returned 
marko as well as josh, and 
`outE().count().as("a").is(gt(1).and(lte(__.select("a"))))` returned 2 instead 
of 6. The strategy only sees the literal part of the predicate, so it can't 
derive a safe limit. `CountStrategy` now leaves `count().is(P)` alone whenever 
`P` contains a traversal. This was already broken before this PR, but it's the 
same class of bug, so I've included the fix and two feature scenarios for it 
here. A narrower rule (keep the limit when an `and()` has a literal part that 
rejects the cap) would be possible as a follow-up, but it needs careful 
handling of `and`/`or`/`not` with unknown values, so I kept it simple here.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to