Skip to content

Commit 8fd9841

Browse files
contrueCTimbajin
andauthored
refactor(server): preserve core gremlin semantics (2/3) (#3262)
Part 2 of upgrade series. Preserve transaction count and predicate semantics while retaining TinkerPop 3.5.1 Stabilize query behavior on TinkerPop 3.5.1: pending removals take precedence in transaction-visible counts, unsupported mixed filters remain in one local HasStep, and count barriers, input multiplicity and iterator cleanup are preserved. --------- Co-authored-by: imbajin <jin@apache.org>
1 parent 39a3a8d commit 8fd9841

8 files changed

Lines changed: 1131 additions & 32 deletions

File tree

‎docs/query-semantics.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Query count and filter behavior
2+
3+
Counts made inside a transaction include that transaction's uncommitted vertex
4+
and edge changes. Only `count()` supports this fallback; other aggregates with
5+
uncommitted changes are rejected. Queries that combine uncommitted changes with
6+
pagination, a limit, or an offset remain unsupported.
7+
8+
Queries and counts select the current transaction version before applying filters.
9+
When an element is removed and re-added with the same ID, the new element shadows
10+
the old updated record, even if only the old record matches the query's label.
11+
12+
A count query that enters an index query path remains unsupported when that index
13+
transaction has uncommitted changes. It reports `Can't do index query when there
14+
are changes in transaction`. This includes indexed-property conditions and
15+
label-only queries on backends that need a label index. Graph-wide and explicit-ID
16+
counts can still use transaction merging without entering that index path.
17+
18+
Unsupported text predicates remain traversal filters. For ordinary GraphStep and
19+
VertexStep extraction, if any condition in a `HasStep` cannot be converted, the
20+
whole step remains local, including its sibling label, ID and property conditions.
21+
Count optimization preserves that filter step. Existing special handling around
22+
`match()` and connective label filters is inherited from master.
23+
24+
New generalized selective pushdown, predicate-specific local ID/SEARCH matching and
25+
candidate-index coverage are outside this upgrade series. They are coordinated
26+
through [PR #2994](https://github.com/apache/hugegraph/pull/2994),
27+
[issue #3201](https://github.com/apache/hugegraph/issues/3201) and
28+
[PR #3243](https://github.com/apache/hugegraph/pull/3243).
29+
30+
A self-loop contributes two occurrences to a vertex's `bothE()` traversal and
31+
one to each of `outE()`, `inE()` and the graph-wide `E()` traversal. Counts retain
32+
these multiplicities before and after transaction commit, including edge updates.
33+
34+
Count optimization preserves steps that can filter candidates. Resetting an
35+
optimized count traversal permits it to execute again. Query-step equality does
36+
not depend on the result iterator from a previous execution.
37+
38+
A scan in the middle of a traversal is counted once per incoming traverser,
39+
including its bulk. It is not replaced by a single backend count. For example,
40+
with three vertices, `g.V().V().count()` returns `9L`, while `g.V().count()`
41+
returns `3L` and remains eligible for count optimization.

‎hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java‎

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import org.apache.hugegraph.iterator.BatchMapperIterator;
6565
import org.apache.hugegraph.iterator.ExtendableIterator;
6666
import org.apache.hugegraph.iterator.FilterIterator;
67+
import org.apache.hugegraph.iterator.FlatMapperIterator;
6768
import org.apache.hugegraph.iterator.LimitIterator;
6869
import org.apache.hugegraph.iterator.MapperIterator;
6970
import org.apache.hugegraph.job.system.DeleteExpiredJob;
@@ -548,24 +549,27 @@ public Number queryNumber(Query query) {
548549
boolean hasUpdate = this.hasUpdate();
549550
Aggregate aggregate = query.aggregateNotNull();
550551

551-
// TODO: we can concat index-query results and tx uncommitted records.
552-
if (hasUpdate) {
553-
E.checkArgument(!isConditionQuery,
554-
"It's not allowed to query by index when " +
555-
"there are uncommitted records.");
552+
if (hasUpdate && (query.resultType() == HugeType.VERTEX ||
553+
query.resultType() == HugeType.EDGE)) {
554+
E.checkArgument(aggregate.func() == AggregateFunc.COUNT,
555+
"The %s operator with uncommitted records " +
556+
"is not supported",
557+
aggregate.func().string());
558+
Query queryWithoutAggregate = query.copy();
559+
queryWithoutAggregate.aggregate(null);
560+
Iterator<?> results = queryWithoutAggregate.resultType().isVertex() ?
561+
this.queryVertices(queryWithoutAggregate) :
562+
this.queryEdges(queryWithoutAggregate);
563+
return countAndClose(results);
556564
}
557565

558566
QueryList<Number> queries = this.optimizeQueries(query, q -> {
559567
boolean isIndexQuery = q instanceof IdQuery;
560568
assert isIndexQuery || isConditionQuery || q == query;
561-
// Need to fall back if there are uncommitted records
562-
boolean fallback = hasUpdate;
569+
boolean fallback = false;
563570
Number result;
564571

565-
if (fallback) {
566-
// Here just ignore it, and do fall back later
567-
result = null;
568-
} else if (!isIndexQuery || !isConditionQuery) {
572+
if (!isIndexQuery || !isConditionQuery) {
569573
// It's a sysprop-query, let parent tx do it
570574
assert !fallback;
571575
result = super.queryNumber(q);
@@ -591,9 +595,9 @@ public Number queryNumber(Query query) {
591595
assert q.resultType().isVertex() || q.resultType().isEdge();
592596
// Reset aggregate to fallback and scan
593597
q.aggregate(null);
594-
result = IteratorUtils.count(q.resultType().isVertex() ?
595-
this.queryVertices(q) :
596-
this.queryEdges(q));
598+
result = countAndClose(q.resultType().isVertex() ?
599+
this.queryVertices(q) :
600+
this.queryEdges(q));
597601
}
598602

599603
return new QueryResults<>(IteratorUtils.of(result), q);
@@ -605,6 +609,19 @@ public Number queryNumber(Query query) {
605609
return aggregate.reduce(results.iterator());
606610
}
607611

612+
private static long countAndClose(Iterator<?> results) {
613+
try {
614+
long count = 0L;
615+
while (results.hasNext()) {
616+
results.next();
617+
count++;
618+
}
619+
return count;
620+
} finally {
621+
CloseableIterator.closeIterator(results);
622+
}
623+
}
624+
608625
@Watched(prefix = "graph")
609626
public HugeVertex addVertex(Object... keyValues) {
610627
return this.addVertex(this.constructVertex(true, keyValues));
@@ -819,7 +836,7 @@ public Iterator<Vertex> queryVertices() {
819836
public Iterator<Vertex> queryVertices(Query query) {
820837
if (this.hasUpdate()) {
821838
E.checkArgument(query.noLimitAndOffset(),
822-
"It's not allowed to query with offser/limit " +
839+
"It's not allowed to query with offset/limit " +
823840
"when there are uncommitted records.");
824841
// TODO: also add check: no SCAN, no OLAP
825842
E.checkArgument(!query.paging(),
@@ -1008,7 +1025,7 @@ public Iterator<Edge> queryEdges() {
10081025
public Iterator<Edge> queryEdges(Query query) {
10091026
if (this.hasUpdate()) {
10101027
E.checkArgument(query.noLimitAndOffset(),
1011-
"It's not allowed to query with offser/limit " +
1028+
"It's not allowed to query with offset/limit " +
10121029
"when there are uncommitted records.");
10131030
// TODO: also add check: no SCAN, no OLAP
10141031
E.checkArgument(!query.paging(),
@@ -2037,6 +2054,22 @@ private Iterator<?> joinTxEdges(Query query, Iterator<HugeEdge> edges,
20372054
edges = this.joinTxRecords(query, edges, matchTxFunc,
20382055
this.addedEdges, this.removedEdges,
20392056
this.updatedEdges);
2057+
if (query instanceof ConditionQuery &&
2058+
((ConditionQuery) query).containsCondition(HugeKeys.OWNER_VERTEX) &&
2059+
!(this.addedEdges.isEmpty() && this.updatedEdges.isEmpty())) {
2060+
edges = new FlatMapperIterator<>(edges, edge -> {
2061+
// Rehydrated self-loops may hold distinct vertex objects.
2062+
if (edge.sourceVertex().id().equals(edge.targetVertex().id()) &&
2063+
(this.addedEdges.containsKey(edge.id()) ||
2064+
this.updatedEdges.containsKey(edge.id()))) {
2065+
HugeEdge opposite = edge.switchOwner();
2066+
if (query.test(opposite)) {
2067+
return ImmutableList.of(edge, opposite).iterator();
2068+
}
2069+
}
2070+
return ImmutableList.of(edge).iterator();
2071+
});
2072+
}
20402073
if (removingVertices.isEmpty()) {
20412074
return edges;
20422075
}
@@ -2073,6 +2106,9 @@ private <V extends HugeElement> Iterator<V> joinTxRecords(
20732106
* Records in memory have higher priority than a query from backend store
20742107
*/
20752108
for (V elem : addedTxRecords.values()) {
2109+
if (removedTxRecords.containsKey(elem.id())) {
2110+
continue;
2111+
}
20762112
if (query.reachLimit(txResults.size())) {
20772113
break;
20782114
}
@@ -2081,6 +2117,9 @@ private <V extends HugeElement> Iterator<V> joinTxRecords(
20812117
}
20822118
}
20832119
for (V elem : updatedTxRecords.values()) {
2120+
if (removedTxRecords.containsKey(elem.id()) || addedTxRecords.containsKey(elem.id())) {
2121+
continue;
2122+
}
20842123
if (query.reachLimit(txResults.size())) {
20852124
break;
20862125
}

‎hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,18 @@ public boolean equals(Object obj) {
5555

5656
HugeCountStep other = (HugeCountStep) obj;
5757
return Objects.equals(this.originGraphStep,
58-
other.originGraphStep) && this.done == other.done;
58+
other.originGraphStep);
5959
}
6060

6161
@Override
6262
public int hashCode() {
63-
return Objects.hash(super.hashCode(), this.originGraphStep, this.done);
63+
return Objects.hash(super.hashCode(), this.originGraphStep);
64+
}
65+
66+
@Override
67+
public void reset() {
68+
super.reset();
69+
this.done = false;
6470
}
6571

6672
@Override

‎hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateLocalStep;
3535
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep;
3636
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectStep;
37-
import org.apache.tinkerpop.gremlin.process.traversal.step.util.CollectingBarrierStep;
3837
import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
3938
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
4039
import org.apache.tinkerpop.gremlin.structure.Element;
@@ -73,11 +72,11 @@ public void apply(Traversal.Admin<?, ?> traversal) {
7372
HugeGraphStep<?, ? extends Element> graphStep = null;
7473
Step<?, ?> step = originStep;
7574
do {
75+
// Only skip steps that preserve the number of traversers.
7676
if (!(step instanceof CountGlobalStep ||
7777
step instanceof GraphStep ||
7878
step instanceof IdentityStep ||
79-
step instanceof NoOpBarrierStep ||
80-
step instanceof CollectingBarrierStep) ||
79+
step instanceof NoOpBarrierStep) ||
8180
(step instanceof TraversalParent &&
8281
TraversalHelper.anyStepRecursively(s -> {
8382
return s instanceof SideEffectStep ||
@@ -94,7 +93,9 @@ public void apply(Traversal.Admin<?, ?> traversal) {
9493
step = step.getPreviousStep();
9594
} while (step != null);
9695

97-
if (graphStep == null) {
96+
// A mid-traversal scan runs once per incoming traverser (including bulk).
97+
if (graphStep == null || !graphStep.isStartStep() ||
98+
traversal.getStartStep() != graphStep) {
9899
return;
99100
}
100101

‎hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,7 @@ public boolean equals(Object obj) {
210210

211211
HugeGraphStep other = (HugeGraphStep) obj;
212212
return this.hasContainers.equals(other.hasContainers) &&
213-
this.queryInfo.equals(other.queryInfo) &&
214-
this.lastTimeResults.equals(other.lastTimeResults);
213+
this.queryInfo.equals(other.queryInfo);
215214
}
216215

217216
@Override

‎hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java‎

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.apache.tinkerpop.gremlin.process.traversal.Order;
6161
import org.apache.tinkerpop.gremlin.process.traversal.P;
6262
import org.apache.tinkerpop.gremlin.process.traversal.Step;
63+
import org.apache.tinkerpop.gremlin.process.traversal.TextP;
6364
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
6465
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
6566
import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
@@ -177,7 +178,8 @@ public static void extractHasContainer(HugeGraphStep<?, ?> newStep,
177178
while (step instanceof HasStep || step instanceof NoOpBarrierStep) {
178179
Step<?, ?> nextStep = step.getNextStep();
179180
if (step instanceof HasStep) {
180-
HasContainerHolder holder = (HasContainerHolder) step;
181+
HasContainerHolder holder =
182+
(HasContainerHolder) step;
181183
boolean connectiveLabelStep =
182184
removeConnectiveLabelStep(step);
183185
/*
@@ -385,7 +387,7 @@ private static boolean isLabelContainer(HasContainer has) {
385387
}
386388

387389
static boolean isPositiveLabelContainer(HasContainer has) {
388-
if (!isLabelContainer(has)) {
390+
if (!isLabelContainer(has) || hasNullLabelValue(has)) {
389391
return false;
390392
}
391393

@@ -541,6 +543,29 @@ private static boolean hasNullPredicate(HasContainer has) {
541543
return false;
542544
}
543545

546+
private static boolean hasNullLabelValue(HasContainer has) {
547+
if (!isLabelContainer(has)) {
548+
return false;
549+
}
550+
551+
List<P<Object>> predicates = new ArrayList<>();
552+
collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
553+
for (P<Object> pred : predicates) {
554+
Object value = pred.getValue();
555+
if (value == null) {
556+
return true;
557+
}
558+
if (value instanceof Collection) {
559+
for (Object label : (Collection<?>) value) {
560+
if (label == null) {
561+
return true;
562+
}
563+
}
564+
}
565+
}
566+
return false;
567+
}
568+
544569
private static boolean hasBooleanIndex(HugeGraph graph,
545570
SchemaLabel schemaLabel,
546571
PropertyKey pkey) {
@@ -607,7 +632,8 @@ public static void extractHasContainer(HugeVertexStep<?> newStep,
607632
Step<?, ?> nextStep = step.getNextStep();
608633
if (step instanceof HasStep) {
609634
removeConnectiveLabelStep(step);
610-
HasContainerHolder holder = (HasContainerHolder) step;
635+
HasContainerHolder holder =
636+
(HasContainerHolder) step;
611637
if (extractHasContainers(newStep, holder)) {
612638
TraversalHelper.copyLabels(step, step.getPreviousStep(), false);
613639
traversal.removeStep(step);
@@ -655,6 +681,11 @@ private static boolean canExtractHasContainers(HugeGraph graph,
655681

656682
static boolean canExtractHasContainer(HugeGraph graph,
657683
HasContainer has) {
684+
if (has.getKey() == null || has.getPredicate() == null ||
685+
hasNullLabelValue(has) ||
686+
hasTextPredicate(has)) {
687+
return false;
688+
}
658689
if (isSysProp(has.getKey())) {
659690
return true;
660691
}
@@ -687,6 +718,17 @@ static boolean canExtractHasContainer(HugeGraph graph,
687718
return true;
688719
}
689720

721+
private static boolean hasTextPredicate(HasContainer has) {
722+
List<P<Object>> predicates = new ArrayList<>();
723+
collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
724+
for (P<Object> predicate : predicates) {
725+
if (TextP.class.isInstance(predicate)) {
726+
return true;
727+
}
728+
}
729+
return false;
730+
}
731+
690732
public static void extractOrder(Step<?, ?> newStep,
691733
Traversal.Admin<?, ?> traversal) {
692734
Step<?, ?> step = newStep;
@@ -1097,6 +1139,9 @@ public static HugeKeys string2HugeKey(String key) {
10971139
}
10981140

10991141
public static HugeKeys token2HugeKey(String key) {
1142+
if (key == null) {
1143+
return null;
1144+
}
11001145
if (key.equals(T.label.getAccessor())) {
11011146
return HugeKeys.LABEL;
11021147
} else if (key.equals(T.id.getAccessor())) {
@@ -1187,7 +1232,7 @@ public static void convHasStep(HugeGraph graph, HasStep<?> step) {
11871232
private static void convPredicateValue(HugeGraph graph,
11881233
HasContainer has) {
11891234
// No need to convert if key is sys-prop
1190-
if (isSysProp(has.getKey())) {
1235+
if (has.getKey() == null || isSysProp(has.getKey())) {
11911236
return;
11921237
}
11931238
PropertyKey pkey = graph.propertyKey(has.getKey());
@@ -1198,15 +1243,24 @@ private static void updatePredicateValue(P<?> predicate, PropertyKey pkey) {
11981243
List<P<Object>> leafPredicates = new ArrayList<>();
11991244
collectPredicates(leafPredicates, ImmutableList.of(predicate));
12001245
for (P<Object> pred : leafPredicates) {
1201-
if (pred.getBiPredicate() == Compare.neq &&
1202-
pred.getValue() == null) {
1246+
if (isNullInequalityPredicate(pred)) {
12031247
continue;
12041248
}
12051249
Object value = validPropertyValue(pred.getValue(), pkey);
12061250
pred.setValue(value);
12071251
}
12081252
}
12091253

1254+
private static boolean isNullInequalityPredicate(P<?> predicate) {
1255+
if (predicate.getValue() != null) {
1256+
return false;
1257+
}
1258+
if (predicate.getBiPredicate() == Compare.neq) {
1259+
return true;
1260+
}
1261+
return false;
1262+
}
1263+
12101264
private static boolean isSysProp(String key) {
12111265
if (QueryHolder.SYSPROP_PAGE.equals(key)) {
12121266
return true;

0 commit comments

Comments
 (0)