Repository navigation
Fix CountStrategy returning capped counts when the count is observed #3717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -68,10 +68,14 @@ public static Iterable<Object[]> 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,59 @@ 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))}, | ||||||||||||||
|
Cole-Greer marked this conversation as resolved.
|
||||||||||||||
| {__.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))}, | ||||||||||||||
| // 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)))}, | ||||||||||||||
|
Comment on lines
+106
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think theoretically these cases could be optimized as
Suggested change
I think the strategy is only looking at the |
||||||||||||||
| // 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().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 | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Shorter changelog entries are preferred, perhaps: