You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hugegraph.apache.org by ji...@apache.org on 2026/10/05 17:02:31 UTC
(hugegraph) branch master updated: refactor(server): preserve core gremlin semantics (2/3) (#3262)
This is an automated email from the ASF dual-hosted git repository.
imbajin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hugegraph.git
The following commit(s) were added to refs/heads/master by this push:
new 8fd9841eb refactor(server): preserve core gremlin semantics (2/3) (#3262)
8fd9841eb is described below
commit 8fd9841ebc6770df6119fdce07249fa9e558ae9e
Author: contrueCT <co...@outlook.com>
AuthorDate: Tue Oct 6 01:02:25 2026 +0800
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 <ji...@apache.org>
---
docs/query-semantics.md | 41 +
.../hugegraph/backend/tx/GraphTransaction.java | 71 +-
.../traversal/optimize/HugeCountStep.java | 10 +-
.../traversal/optimize/HugeCountStepStrategy.java | 9 +-
.../traversal/optimize/HugeGraphStep.java | 3 +-
.../traversal/optimize/TraversalUtil.java | 66 +-
.../hugegraph/core/CountStrategyCoreTest.java | 865 ++++++++++++++++++++-
.../optimize/TraversalUtilOptimizeTest.java | 98 ++-
8 files changed, 1131 insertions(+), 32 deletions(-)
diff --git a/docs/query-semantics.md b/docs/query-semantics.md
new file mode 100644
index 000000000..5cfb410ce
--- /dev/null
+++ b/docs/query-semantics.md
@@ -0,0 +1,41 @@
+# Query count and filter behavior
+
+Counts made inside a transaction include that transaction's uncommitted vertex
+and edge changes. Only `count()` supports this fallback; other aggregates with
+uncommitted changes are rejected. Queries that combine uncommitted changes with
+pagination, a limit, or an offset remain unsupported.
+
+Queries and counts select the current transaction version before applying filters.
+When an element is removed and re-added with the same ID, the new element shadows
+the old updated record, even if only the old record matches the query's label.
+
+A count query that enters an index query path remains unsupported when that index
+transaction has uncommitted changes. It reports `Can't do index query when there
+are changes in transaction`. This includes indexed-property conditions and
+label-only queries on backends that need a label index. Graph-wide and explicit-ID
+counts can still use transaction merging without entering that index path.
+
+Unsupported text predicates remain traversal filters. For ordinary GraphStep and
+VertexStep extraction, if any condition in a `HasStep` cannot be converted, the
+whole step remains local, including its sibling label, ID and property conditions.
+Count optimization preserves that filter step. Existing special handling around
+`match()` and connective label filters is inherited from master.
+
+New generalized selective pushdown, predicate-specific local ID/SEARCH matching and
+candidate-index coverage are outside this upgrade series. They are coordinated
+through [PR #2994](https://github.com/apache/hugegraph/pull/2994),
+[issue #3201](https://github.com/apache/hugegraph/issues/3201) and
+[PR #3243](https://github.com/apache/hugegraph/pull/3243).
+
+A self-loop contributes two occurrences to a vertex's `bothE()` traversal and
+one to each of `outE()`, `inE()` and the graph-wide `E()` traversal. Counts retain
+these multiplicities before and after transaction commit, including edge updates.
+
+Count optimization preserves steps that can filter candidates. Resetting an
+optimized count traversal permits it to execute again. Query-step equality does
+not depend on the result iterator from a previous execution.
+
+A scan in the middle of a traversal is counted once per incoming traverser,
+including its bulk. It is not replaced by a single backend count. For example,
+with three vertices, `g.V().V().count()` returns `9L`, while `g.V().count()`
+returns `3L` and remains eligible for count optimization.
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java
index b22fb1dad..77ea7784e 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java
@@ -64,6 +64,7 @@ import org.apache.hugegraph.exception.NotFoundException;
import org.apache.hugegraph.iterator.BatchMapperIterator;
import org.apache.hugegraph.iterator.ExtendableIterator;
import org.apache.hugegraph.iterator.FilterIterator;
+import org.apache.hugegraph.iterator.FlatMapperIterator;
import org.apache.hugegraph.iterator.LimitIterator;
import org.apache.hugegraph.iterator.MapperIterator;
import org.apache.hugegraph.job.system.DeleteExpiredJob;
@@ -548,24 +549,27 @@ public class GraphTransaction extends IndexableTransaction {
boolean hasUpdate = this.hasUpdate();
Aggregate aggregate = query.aggregateNotNull();
- // TODO: we can concat index-query results and tx uncommitted records.
- if (hasUpdate) {
- E.checkArgument(!isConditionQuery,
- "It's not allowed to query by index when " +
- "there are uncommitted records.");
+ if (hasUpdate && (query.resultType() == HugeType.VERTEX ||
+ query.resultType() == HugeType.EDGE)) {
+ E.checkArgument(aggregate.func() == AggregateFunc.COUNT,
+ "The %s operator with uncommitted records " +
+ "is not supported",
+ aggregate.func().string());
+ Query queryWithoutAggregate = query.copy();
+ queryWithoutAggregate.aggregate(null);
+ Iterator<?> results = queryWithoutAggregate.resultType().isVertex() ?
+ this.queryVertices(queryWithoutAggregate) :
+ this.queryEdges(queryWithoutAggregate);
+ return countAndClose(results);
}
QueryList<Number> queries = this.optimizeQueries(query, q -> {
boolean isIndexQuery = q instanceof IdQuery;
assert isIndexQuery || isConditionQuery || q == query;
- // Need to fall back if there are uncommitted records
- boolean fallback = hasUpdate;
+ boolean fallback = false;
Number result;
- if (fallback) {
- // Here just ignore it, and do fall back later
- result = null;
- } else if (!isIndexQuery || !isConditionQuery) {
+ if (!isIndexQuery || !isConditionQuery) {
// It's a sysprop-query, let parent tx do it
assert !fallback;
result = super.queryNumber(q);
@@ -591,9 +595,9 @@ public class GraphTransaction extends IndexableTransaction {
assert q.resultType().isVertex() || q.resultType().isEdge();
// Reset aggregate to fallback and scan
q.aggregate(null);
- result = IteratorUtils.count(q.resultType().isVertex() ?
- this.queryVertices(q) :
- this.queryEdges(q));
+ result = countAndClose(q.resultType().isVertex() ?
+ this.queryVertices(q) :
+ this.queryEdges(q));
}
return new QueryResults<>(IteratorUtils.of(result), q);
@@ -605,6 +609,19 @@ public class GraphTransaction extends IndexableTransaction {
return aggregate.reduce(results.iterator());
}
+ private static long countAndClose(Iterator<?> results) {
+ try {
+ long count = 0L;
+ while (results.hasNext()) {
+ results.next();
+ count++;
+ }
+ return count;
+ } finally {
+ CloseableIterator.closeIterator(results);
+ }
+ }
+
@Watched(prefix = "graph")
public HugeVertex addVertex(Object... keyValues) {
return this.addVertex(this.constructVertex(true, keyValues));
@@ -819,7 +836,7 @@ public class GraphTransaction extends IndexableTransaction {
public Iterator<Vertex> queryVertices(Query query) {
if (this.hasUpdate()) {
E.checkArgument(query.noLimitAndOffset(),
- "It's not allowed to query with offser/limit " +
+ "It's not allowed to query with offset/limit " +
"when there are uncommitted records.");
// TODO: also add check: no SCAN, no OLAP
E.checkArgument(!query.paging(),
@@ -1008,7 +1025,7 @@ public class GraphTransaction extends IndexableTransaction {
public Iterator<Edge> queryEdges(Query query) {
if (this.hasUpdate()) {
E.checkArgument(query.noLimitAndOffset(),
- "It's not allowed to query with offser/limit " +
+ "It's not allowed to query with offset/limit " +
"when there are uncommitted records.");
// TODO: also add check: no SCAN, no OLAP
E.checkArgument(!query.paging(),
@@ -2037,6 +2054,22 @@ public class GraphTransaction extends IndexableTransaction {
edges = this.joinTxRecords(query, edges, matchTxFunc,
this.addedEdges, this.removedEdges,
this.updatedEdges);
+ if (query instanceof ConditionQuery &&
+ ((ConditionQuery) query).containsCondition(HugeKeys.OWNER_VERTEX) &&
+ !(this.addedEdges.isEmpty() && this.updatedEdges.isEmpty())) {
+ edges = new FlatMapperIterator<>(edges, edge -> {
+ // Rehydrated self-loops may hold distinct vertex objects.
+ if (edge.sourceVertex().id().equals(edge.targetVertex().id()) &&
+ (this.addedEdges.containsKey(edge.id()) ||
+ this.updatedEdges.containsKey(edge.id()))) {
+ HugeEdge opposite = edge.switchOwner();
+ if (query.test(opposite)) {
+ return ImmutableList.of(edge, opposite).iterator();
+ }
+ }
+ return ImmutableList.of(edge).iterator();
+ });
+ }
if (removingVertices.isEmpty()) {
return edges;
}
@@ -2073,6 +2106,9 @@ public class GraphTransaction extends IndexableTransaction {
* Records in memory have higher priority than a query from backend store
*/
for (V elem : addedTxRecords.values()) {
+ if (removedTxRecords.containsKey(elem.id())) {
+ continue;
+ }
if (query.reachLimit(txResults.size())) {
break;
}
@@ -2081,6 +2117,9 @@ public class GraphTransaction extends IndexableTransaction {
}
}
for (V elem : updatedTxRecords.values()) {
+ if (removedTxRecords.containsKey(elem.id()) || addedTxRecords.containsKey(elem.id())) {
+ continue;
+ }
if (query.reachLimit(txResults.size())) {
break;
}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
index 403bf5be8..f12a84e40 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStep.java
@@ -55,12 +55,18 @@ public final class HugeCountStep<S extends Element>
HugeCountStep other = (HugeCountStep) obj;
return Objects.equals(this.originGraphStep,
- other.originGraphStep) && this.done == other.done;
+ other.originGraphStep);
}
@Override
public int hashCode() {
- return Objects.hash(super.hashCode(), this.originGraphStep, this.done);
+ return Objects.hash(super.hashCode(), this.originGraphStep);
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ this.done = false;
}
@Override
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java
index ef380d36b..b4ef2500e 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStepStrategy.java
@@ -34,7 +34,6 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateG
import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateLocalStep;
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.util.CollectingBarrierStep;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.structure.Element;
@@ -73,11 +72,11 @@ public final class HugeCountStepStrategy
HugeGraphStep<?, ? extends Element> graphStep = null;
Step<?, ?> step = originStep;
do {
+ // Only skip steps that preserve the number of traversers.
if (!(step instanceof CountGlobalStep ||
step instanceof GraphStep ||
step instanceof IdentityStep ||
- step instanceof NoOpBarrierStep ||
- step instanceof CollectingBarrierStep) ||
+ step instanceof NoOpBarrierStep) ||
(step instanceof TraversalParent &&
TraversalHelper.anyStepRecursively(s -> {
return s instanceof SideEffectStep ||
@@ -94,7 +93,9 @@ public final class HugeCountStepStrategy
step = step.getPreviousStep();
} while (step != null);
- if (graphStep == null) {
+ // A mid-traversal scan runs once per incoming traverser (including bulk).
+ if (graphStep == null || !graphStep.isStartStep() ||
+ traversal.getStartStep() != graphStep) {
return;
}
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java
index bdfb9e0b6..48381241a 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeGraphStep.java
@@ -210,8 +210,7 @@ public final class HugeGraphStep<S, E extends Element>
HugeGraphStep other = (HugeGraphStep) obj;
return this.hasContainers.equals(other.hasContainers) &&
- this.queryInfo.equals(other.queryInfo) &&
- this.lastTimeResults.equals(other.lastTimeResults);
+ this.queryInfo.equals(other.queryInfo);
}
@Override
diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
index e6a56027a..6a9f9394d 100644
--- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
+++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtil.java
@@ -60,6 +60,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Contains;
import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
@@ -177,7 +178,8 @@ public final class TraversalUtil {
while (step instanceof HasStep || step instanceof NoOpBarrierStep) {
Step<?, ?> nextStep = step.getNextStep();
if (step instanceof HasStep) {
- HasContainerHolder holder = (HasContainerHolder) step;
+ HasContainerHolder holder =
+ (HasContainerHolder) step;
boolean connectiveLabelStep =
removeConnectiveLabelStep(step);
/*
@@ -385,7 +387,7 @@ public final class TraversalUtil {
}
static boolean isPositiveLabelContainer(HasContainer has) {
- if (!isLabelContainer(has)) {
+ if (!isLabelContainer(has) || hasNullLabelValue(has)) {
return false;
}
@@ -541,6 +543,29 @@ public final class TraversalUtil {
return false;
}
+ private static boolean hasNullLabelValue(HasContainer has) {
+ if (!isLabelContainer(has)) {
+ return false;
+ }
+
+ List<P<Object>> predicates = new ArrayList<>();
+ collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+ for (P<Object> pred : predicates) {
+ Object value = pred.getValue();
+ if (value == null) {
+ return true;
+ }
+ if (value instanceof Collection) {
+ for (Object label : (Collection<?>) value) {
+ if (label == null) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
private static boolean hasBooleanIndex(HugeGraph graph,
SchemaLabel schemaLabel,
PropertyKey pkey) {
@@ -607,7 +632,8 @@ public final class TraversalUtil {
Step<?, ?> nextStep = step.getNextStep();
if (step instanceof HasStep) {
removeConnectiveLabelStep(step);
- HasContainerHolder holder = (HasContainerHolder) step;
+ HasContainerHolder holder =
+ (HasContainerHolder) step;
if (extractHasContainers(newStep, holder)) {
TraversalHelper.copyLabels(step, step.getPreviousStep(), false);
traversal.removeStep(step);
@@ -655,6 +681,11 @@ public final class TraversalUtil {
static boolean canExtractHasContainer(HugeGraph graph,
HasContainer has) {
+ if (has.getKey() == null || has.getPredicate() == null ||
+ hasNullLabelValue(has) ||
+ hasTextPredicate(has)) {
+ return false;
+ }
if (isSysProp(has.getKey())) {
return true;
}
@@ -687,6 +718,17 @@ public final class TraversalUtil {
return true;
}
+ private static boolean hasTextPredicate(HasContainer has) {
+ List<P<Object>> predicates = new ArrayList<>();
+ collectPredicates(predicates, ImmutableList.of(has.getPredicate()));
+ for (P<Object> predicate : predicates) {
+ if (TextP.class.isInstance(predicate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public static void extractOrder(Step<?, ?> newStep,
Traversal.Admin<?, ?> traversal) {
Step<?, ?> step = newStep;
@@ -1097,6 +1139,9 @@ public final class TraversalUtil {
}
public static HugeKeys token2HugeKey(String key) {
+ if (key == null) {
+ return null;
+ }
if (key.equals(T.label.getAccessor())) {
return HugeKeys.LABEL;
} else if (key.equals(T.id.getAccessor())) {
@@ -1187,7 +1232,7 @@ public final class TraversalUtil {
private static void convPredicateValue(HugeGraph graph,
HasContainer has) {
// No need to convert if key is sys-prop
- if (isSysProp(has.getKey())) {
+ if (has.getKey() == null || isSysProp(has.getKey())) {
return;
}
PropertyKey pkey = graph.propertyKey(has.getKey());
@@ -1198,8 +1243,7 @@ public final class TraversalUtil {
List<P<Object>> leafPredicates = new ArrayList<>();
collectPredicates(leafPredicates, ImmutableList.of(predicate));
for (P<Object> pred : leafPredicates) {
- if (pred.getBiPredicate() == Compare.neq &&
- pred.getValue() == null) {
+ if (isNullInequalityPredicate(pred)) {
continue;
}
Object value = validPropertyValue(pred.getValue(), pkey);
@@ -1207,6 +1251,16 @@ public final class TraversalUtil {
}
}
+ private static boolean isNullInequalityPredicate(P<?> predicate) {
+ if (predicate.getValue() != null) {
+ return false;
+ }
+ if (predicate.getBiPredicate() == Compare.neq) {
+ return true;
+ }
+ return false;
+ }
+
private static boolean isSysProp(String key) {
if (QueryHolder.SYSPROP_PAGE.equals(key)) {
return true;
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
index f00918050..693b2404b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
@@ -17,23 +17,53 @@
package org.apache.hugegraph.core;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.hugegraph.HugeException;
+import org.apache.hugegraph.backend.query.Aggregate;
+import org.apache.hugegraph.backend.query.Aggregate.AggregateFunc;
+import org.apache.hugegraph.backend.query.Condition;
+import org.apache.hugegraph.backend.query.ConditionQuery;
+import org.apache.hugegraph.backend.query.Query;
+import org.apache.hugegraph.backend.tx.GraphTransaction;
import org.apache.hugegraph.exception.NoIndexException;
import org.apache.hugegraph.schema.SchemaManager;
import org.apache.hugegraph.testutil.Assert;
+import org.apache.hugegraph.traversal.optimize.ConditionP;
+import org.apache.hugegraph.traversal.optimize.HugeCountStep;
+import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy;
+import org.apache.hugegraph.traversal.optimize.HugeCountStrategy;
import org.apache.hugegraph.traversal.optimize.HugeGraphStep;
+import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy;
+import org.apache.hugegraph.type.HugeType;
+import org.apache.hugegraph.type.define.HugeKeys;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
+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.OrderGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
import org.junit.Test;
public class CountStrategyCoreTest extends BaseCoreTest {
@@ -101,7 +131,8 @@ public class CountStrategyCoreTest extends BaseCoreTest {
if (!(step instanceof HasStep)) {
continue;
}
- HasContainerHolder holder = (HasContainerHolder) step;
+ HasContainerHolder holder =
+ (HasContainerHolder) step;
for (HasContainer has : holder.getHasContainers()) {
if (key.equals(has.getKey())) {
return true;
@@ -111,6 +142,38 @@ public class CountStrategyCoreTest extends BaseCoreTest {
return false;
}
+ private void assertNegatedBooleanPredicate(long expected,
+ P<Boolean> predicate) {
+ GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
+ .has("vp2",
+ P.not(predicate))
+ .count();
+ traversal.asAdmin().applyStrategies();
+
+ Assert.assertTrue(hasRemainingHasStep(traversal, "vp2"));
+ Assert.assertEquals(expected, traversal.next().longValue());
+ }
+
+ private static void assertUncommittedRangeUnsupported(
+ GraphTraversal<?, ?> traversal) {
+ Assert.assertThrows(IllegalArgumentException.class, traversal::next,
+ e -> {
+ Assert.assertContains("offset/limit", e.getMessage());
+ Assert.assertContains("uncommitted records", e.getMessage());
+ });
+ }
+
+ private static void assertNegatedCountHighRange(long expected,
+ P<Long> predicate) {
+ GraphTraversal<?, Long> traversal = __.count().is(P.not(predicate));
+ HugeCountStrategy.instance().apply(traversal.asAdmin());
+
+ Step<?, ?> firstStep = traversal.asAdmin().getStartStep();
+ Assert.assertInstanceOf(RangeGlobalStep.class, firstStep);
+ Assert.assertEquals(expected,
+ ((RangeGlobalStep<?>) firstStep).getHighRange());
+ }
+
private void initTextRangeSchema(boolean withEdge) {
SchemaManager schema = graph().schema();
schema.propertyKey("vp4").asText().create();
@@ -134,6 +197,89 @@ public class CountStrategyCoreTest extends BaseCoreTest {
.nullableKeys("ep4").link("vl1", "vl1").create();
}
+ private void initNegatedDoubleSchema() {
+ SchemaManager schema = graph().schema();
+ schema.propertyKey("score").asDouble().create();
+ schema.vertexLabel("sample").properties("score").create();
+ schema.indexLabel("sampleByScore").onV("sample")
+ .by("score").range().create();
+ }
+
+ private static void assertUnoptimizedCount(long expected,
+ GraphTraversal<?, Long> traversal) {
+ traversal.asAdmin().applyStrategies();
+
+ Assert.assertInstanceOf(CountGlobalStep.class, traversal.asAdmin().getEndStep());
+ Assert.assertFalse(traversal.asAdmin().getSteps().stream()
+ .anyMatch(step -> step instanceof HugeCountStep));
+ Assert.assertEquals(Collections.singletonList(expected), traversal.toList());
+ }
+
+ private void assertRepeatedGraphCounts(long vertices) {
+ assertUnoptimizedCount(vertices * vertices,
+ graph().traversal().V().V().count());
+ assertUnoptimizedCount(2L * vertices,
+ graph().traversal().inject(1, 2).V().count());
+ // Repeated inputs can be bulked, but each must still contribute a scan.
+ assertUnoptimizedCount(2L * vertices,
+ graph().traversal().inject(1, 1).barrier().V().count());
+
+ GraphTraversal<Vertex, Long> root = graph().traversal().V().count();
+ root.asAdmin().applyStrategies();
+ Assert.assertInstanceOf(HugeCountStep.class, root.asAdmin().getStartStep());
+ Assert.assertEquals(Collections.singletonList(vertices), root.toList());
+ }
+
+ @Test
+ public void testRepeatedGraphCountOnEmptyGraph() {
+ this.initSchema();
+
+ this.assertRepeatedGraphCounts(0L);
+ }
+
+ @Test
+ public void testRepeatedGraphCountOnSingleVertex() {
+ this.initSchema();
+ graph().addVertex(T.label, "person", "name", "marko");
+ commitTx();
+
+ this.assertRepeatedGraphCounts(1L);
+ }
+
+ @Test
+ public void testRepeatedGraphCountOnMultipleVertices() {
+ this.initSchema();
+ this.initGraph();
+
+ this.assertRepeatedGraphCounts(3L);
+ }
+
+ @Test
+ public void testRepeatedGraphCountWithIds() {
+ this.initSchema();
+ this.initGraph();
+ Object id = graph().traversal().V().next().id();
+
+ assertUnoptimizedCount(3L, graph().traversal().V().V(id).count());
+ assertUnoptimizedCount(2L, graph().traversal().inject(1, 2).V(id).count());
+ assertUnoptimizedCount(1L, graph().traversal().V(id).V(id).count());
+ }
+
+ @Test
+ public void testCountAfterOrderByPresentProperty() {
+ this.initSchema();
+ this.initGraph();
+
+ GraphTraversal<Vertex, Long> traversal = graph().traversal().V()
+ .order().by("name").count();
+ traversal.asAdmin().applyStrategies();
+
+ Assert.assertFalse(traversal.asAdmin().getEndStep() instanceof HugeCountStep);
+ Assert.assertTrue(traversal.asAdmin().getSteps().stream()
+ .anyMatch(step -> step instanceof OrderGlobalStep));
+ Assert.assertEquals(3L, traversal.next().longValue());
+ }
+
@Test
public void testWhereCountLtNegativeIsAlwaysFalse() {
this.initSchema();
@@ -246,6 +392,63 @@ public class CountStrategyCoreTest extends BaseCoreTest {
Assert.assertEquals(1L, count);
}
+ @Test
+ public void testOptimizedGraphCountCanBeResetAndReused() {
+ this.initSchema();
+ this.initGraph();
+
+ GraphTraversal<Vertex, Long> traversal = graph().traversal().V().count();
+
+ Assert.assertEquals(3L, traversal.next());
+
+ traversal.asAdmin().reset();
+
+ Assert.assertEquals(3L, traversal.next());
+ }
+
+ @Test
+ public void testOptimizedGraphCountEqualityIgnoresExecutionState() {
+ this.initSchema();
+ this.initGraph();
+
+ GraphTraversal<Vertex, Long> first = graph().traversal().V().count();
+ GraphTraversal<Vertex, Long> second = graph().traversal().V().count();
+ first.asAdmin().applyStrategies();
+ second.asAdmin().applyStrategies();
+
+ Step<?, ?> firstStep = first.asAdmin().getEndStep();
+ Step<?, ?> secondStep = second.asAdmin().getEndStep();
+ Assert.assertInstanceOf(HugeCountStep.class, firstStep);
+ Assert.assertInstanceOf(HugeCountStep.class, secondStep);
+ Assert.assertEquals(firstStep, secondStep);
+
+ int hashCode = firstStep.hashCode();
+ Set<Step<?, ?>> steps = new HashSet<>();
+ steps.add(firstStep);
+
+ Assert.assertEquals(3L, first.next());
+
+ Assert.assertEquals(hashCode, firstStep.hashCode());
+ Assert.assertEquals(firstStep, secondStep);
+ Assert.assertTrue(steps.contains(firstStep));
+ }
+
+ @Test
+ public void testOptimizedGraphCountIncludesUncommittedRecords() {
+ this.initSchema();
+ graph().schema().indexLabel("personByName")
+ .onV("person").by("name").create();
+
+ graph().addVertex(T.label, "person", "name", "marko");
+
+ long count = graph().traversal().V()
+ .hasLabel("person")
+ .has("name", "marko")
+ .count().next();
+
+ Assert.assertEquals(1L, count);
+ }
+
@Test
public void testWhereCountFlatAndContradictionEmpty() {
this.initSchema();
@@ -338,6 +541,666 @@ public class CountStrategyCoreTest extends BaseCoreTest {
Assert.assertEquals(1L, count);
}
+ @Test
+ public void testVertexLimitCountRejectsUncommittedAddition() {
+ this.initSchema();
+ graph().addVertex(T.label, "person", "name", "marko");
+
+ assertUncommittedRangeUnsupported(
+ graph().traversal().V().limit(1L).count());
+ }
+
+ @Test
+ public void testVertexRangeCountRejectsUncommittedDeletion() {
+ this.initSchema();
+ graph().schema().indexLabel("personByName")
+ .onV("person").by("name").create();
+ this.initGraph();
+ Vertex marko = graph().traversal().V()
+ .hasLabel("person")
+ .has("name", "marko")
+ .next();
+ marko.remove();
+
+ assertUncommittedRangeUnsupported(
+ graph().traversal().V().range(1L, 3L).count());
+ }
+
+ @Test
+ public void testSystemCountsIgnoreUncommittedVertices() {
+ this.initSchema();
+ for (HugeType type : new HugeType[]{HugeType.TASK, HugeType.SERVER}) {
+ Query query = new Query(type);
+ query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+ long before = graph().queryNumber(query).longValue();
+ graph().addVertex(T.label, "person", "name", "uncommitted");
+ Assert.assertEquals(before, graph().queryNumber(query).longValue());
+ graph().tx().rollback();
+ }
+ }
+
+ @Test
+ public void testQueryNumberKeepsOriginalAggregate() {
+ this.initSchema();
+ graph().addVertex(T.label, "person", "name", "marko");
+
+ Query query = new Query(HugeType.VERTEX);
+ Aggregate aggregate = new Aggregate(AggregateFunc.COUNT, null);
+ query.aggregate(aggregate);
+
+ Assert.assertEquals(1L, graph().queryNumber(query).longValue());
+ Assert.assertSame(aggregate, query.aggregate());
+ }
+
+ @Test
+ public void testUncommittedVertexCountClosesIteratorOnFailure() {
+ CountCloseableIterator<Vertex> vertices =
+ new CountCloseableIterator<>(true);
+ AtomicBoolean dirty = new AtomicBoolean(true);
+ GraphTransaction transaction =
+ this.newCountTransaction(vertices, null, dirty);
+
+ try {
+ Query query = countQuery(HugeType.VERTEX);
+ Assert.assertThrows(IllegalStateException.class,
+ () -> transaction.queryNumber(query));
+ Assert.assertTrue(vertices.closed());
+ } finally {
+ dirty.set(false);
+ transaction.close();
+ }
+ }
+
+ @Test
+ public void testUncommittedEdgeCountClosesIteratorOnFailure() {
+ CountCloseableIterator<Edge> edges =
+ new CountCloseableIterator<>(true);
+ AtomicBoolean dirty = new AtomicBoolean(true);
+ GraphTransaction transaction =
+ this.newCountTransaction(null, edges, dirty);
+
+ try {
+ Query query = countQuery(HugeType.EDGE);
+ Assert.assertThrows(IllegalStateException.class,
+ () -> transaction.queryNumber(query));
+ Assert.assertTrue(edges.closed());
+ } finally {
+ dirty.set(false);
+ transaction.close();
+ }
+ }
+
+ @Test
+ public void testCommittedVertexCountClosesIterator() {
+ this.assertCommittedVertexCountClosesIterator(false);
+ }
+
+ @Test
+ public void testCommittedVertexCountClosesIteratorOnFailure() {
+ this.assertCommittedVertexCountClosesIterator(true);
+ }
+
+ private void assertCommittedVertexCountClosesIterator(boolean fail) {
+ SchemaManager schema = graph().schema();
+ schema.propertyKey("name").asText().create();
+ schema.vertexLabel("person").properties("name")
+ .primaryKeys("name").create();
+
+ ConditionQuery query = new ConditionQuery(HugeType.VERTEX);
+ query.eq(HugeKeys.LABEL, graph().vertexLabel("person").id());
+ query.query(Condition.eq(graph().propertyKey("name").id(), "marko"));
+ query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+
+ CountCloseableIterator<Vertex> vertices = new CountCloseableIterator<>(fail);
+ GraphTransaction transaction = this.newCountTransaction(
+ vertices, null, new AtomicBoolean(false));
+ try {
+ // Primary-key optimization produces an IdQuery that must scan to count.
+ if (fail) {
+ Assert.assertThrows(IllegalStateException.class,
+ () -> transaction.queryNumber(query));
+ } else {
+ Assert.assertEquals(1L, transaction.queryNumber(query).longValue());
+ }
+ Assert.assertTrue(vertices.closed());
+ } finally {
+ transaction.close();
+ }
+ }
+
+ @Test
+ public void testLabelPredicatesWithNullHostileCollections() {
+ this.initSchema();
+ Vertex person = graph().addVertex(T.label, "person", "name", "marko");
+ Vertex software = graph().addVertex(T.label, "software", "name", "market");
+ commitTx();
+
+ List<Collection<String>> values = List.of(List.of("person"), Set.of("person"));
+ for (Collection<String> labels : values) {
+ Assert.assertEquals(Collections.singletonList(person),
+ graph().traversal().V().hasLabel(P.within(labels)).toList());
+ Assert.assertEquals(1L, graph().traversal().V().hasLabel(P.within(labels))
+ .count().next().longValue());
+ GraphTraversal<Vertex, Vertex> without = graph().traversal().V()
+ .hasLabel(P.without(labels)).has("name", TextP.containing("ar"));
+ without.asAdmin().applyStrategies();
+ Assert.assertTrue(hasRemainingHasStep(without, T.label.getAccessor()));
+ Assert.assertEquals(Collections.singletonList(software), without.toList());
+ }
+ }
+
+ @Test
+ public void testLabelPredicatesWithMutableNullValuesStayLocal() {
+ this.initSchema();
+ Vertex person = graph().addVertex(T.label, "person", "name", "marko");
+ Vertex software = graph().addVertex(T.label, "software", "name", "market");
+ commitTx();
+
+ Collection<String> list = new ArrayList<>(List.of("person"));
+ list.add(null);
+ Collection<String> set = new HashSet<>(list);
+ for (Collection<String> labels : List.of(list, set)) {
+ GraphTraversal<Vertex, Vertex> within = graph().traversal().V()
+ .hasLabel(P.within(labels));
+ within.asAdmin().applyStrategies();
+ Assert.assertTrue(hasRemainingHasStep(within, T.label.getAccessor()));
+ Assert.assertEquals(Collections.singletonList(person), within.toList());
+ GraphTraversal<Vertex, Vertex> without = graph().traversal().V()
+ .hasLabel(P.without(labels));
+ without.asAdmin().applyStrategies();
+ Assert.assertTrue(hasRemainingHasStep(without, T.label.getAccessor()));
+ Assert.assertEquals(Collections.singletonList(software), without.toList());
+ }
+ }
+
+ @Test
+ public void testMixedTextNativeIdFilterKeepsAllConditionsLocal() {
+ this.initSchema();
+ Vertex josh = graph().addVertex(T.label, "person", "name", "josh");
+ Vertex marko = graph().addVertex(T.label, "person", "name", "marko");
+ commitTx();
+
+ GraphTraversalSource[] sources = {
+ graph().traversal(),
+ graph().traversal().withoutStrategies(HugeGraphStepStrategy.class,
+ HugeCountStepStrategy.class)
+ };
+ for (GraphTraversalSource source : sources) {
+ assertMixedTextNativeIdFilter(source, marko.id(), "ar",
+ Collections.singletonList(marko));
+ assertMixedTextNativeIdFilter(source, josh.id(), "ar",
+ Collections.emptyList());
+ assertMixedTextNativeIdFilter(source, marko.id(), "osh",
+ Collections.emptyList());
+ Assert.assertEquals(Collections.singletonList(marko),
+ source.V().hasId(marko.id()).toList());
+ Assert.assertEquals(1L, source.V().hasId(P.eq(marko.id()))
+ .count().next().longValue());
+ }
+ }
+
+ private static void assertMixedTextNativeIdFilter(GraphTraversalSource source,
+ Object id, String text,
+ List<Vertex> expected) {
+ GraphTraversal<Vertex, Vertex> traversal = nativeIdTextQuery(source, id, text);
+ traversal.asAdmin().applyStrategies();
+ assertMixedTextNativeIdPlan(traversal);
+ Assert.assertEquals(expected, traversal.toList());
+
+ GraphTraversal<Vertex, Long> count = nativeIdTextQuery(source, id, text).count();
+ assertUnoptimizedCount(expected.size(), count);
+ assertMixedTextNativeIdPlan(count);
+ Assert.assertEquals(expected, nativeIdTextQuery(source, id, text)
+ .limit(1).toList());
+ Assert.assertEquals(Collections.emptyList(),
+ nativeIdTextQuery(source, id, text).range(1, 2).toList());
+ }
+
+ private static GraphTraversal<Vertex, Vertex> nativeIdTextQuery(
+ GraphTraversalSource source, Object id, String text) {
+ return source.V().hasId(ConditionP.eq(id))
+ .has("name", TextP.containing(text));
+ }
+
+ private static void assertMixedTextNativeIdPlan(GraphTraversal<?, ?> traversal) {
+ Assert.assertTrue(hasRemainingHasStep(traversal, T.id.getAccessor()));
+ Assert.assertTrue(hasRemainingHasStep(traversal, "name"));
+ Assert.assertEquals(0, ((GraphStep<?, ?>) traversal.asAdmin()
+ .getStartStep()).getIds().length);
+ }
+
+ @Test
+ public void testMixedTextEdgeFilterKeepsAllConditionsLocal() {
+ this.initSchema();
+ graph().schema().propertyKey("weight").asInt().create();
+ graph().schema().edgeLabel("rated").link("person", "person")
+ .properties("name", "weight").create();
+ Vertex source = graph().addVertex(T.label, "person", "name", "source");
+ Vertex target = graph().addVertex(T.label, "person", "name", "target");
+ source.addEdge("rated", target, "name", "marko", "weight", 1);
+ Vertex other = graph().addVertex(T.label, "person", "name", "other");
+ Edge expected = source.addEdge("rated", other,
+ "name", "marko", "weight", 2);
+ commitTx();
+
+ List<Edge> edges = graph().traversal().V(source.id()).outE("rated")
+ .has("name", TextP.containing("ar"))
+ .has("weight", 2).toList();
+ Assert.assertEquals(1, edges.size());
+ Assert.assertEquals(expected.id(), edges.get(0).id());
+ Assert.assertEquals(1L, graph().traversal().V(source.id()).outE("rated")
+ .has("name", TextP.containing("ar"))
+ .has("weight", 2).count().next().longValue());
+ }
+
+ @Test
+ public void testUpdatedThenRemovedVertexIsAbsentFromCounts() {
+ this.initSchema();
+ Vertex vertex = graph().addVertex(T.label, "person", "name", "before");
+ commitTx();
+ Object id = vertex.id();
+ vertex = graph().traversal().V(id).next();
+ vertex.property("name", "after");
+ vertex.remove();
+
+ this.assertRemovedVertexCounts(id);
+ graph().tx().rollback();
+ Assert.assertEquals(1L, graph().traversal().V(id).count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().V().hasLabel("person")
+ .count().next().longValue());
+ Assert.assertEquals("before", graph().traversal().V(id).values("name").next());
+ }
+
+ private void assertRemovedVertexCounts(Object id) {
+ Assert.assertEquals(0L, graph().traversal().V(id).count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().V().hasLabel("person")
+ .count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().V().count().next().longValue());
+ Assert.assertTrue(graph().traversal().V().toList().isEmpty());
+ Assert.assertEquals(0L, graph().traversal().V()
+ .has("name", TextP.containing("after"))
+ .count().next().longValue());
+ }
+
+ @Test
+ public void testUpdatedThenRemovedEdgeIsAbsentFromCounts() {
+ this.assertUpdatedThenRemovedEdgeCounts(false);
+ }
+
+ @Test
+ public void testUpdatedThenRemovedSelfLoopIsAbsentFromCounts() {
+ this.assertUpdatedThenRemovedEdgeCounts(true);
+ }
+
+ private void assertUpdatedThenRemovedEdgeCounts(boolean selfLoop) {
+ this.initSchema();
+ graph().schema().edgeLabel("rated").link("person", "person")
+ .properties("name").create();
+ Vertex source = graph().addVertex(T.label, "person", "name", "source");
+ Vertex target = selfLoop ? source :
+ graph().addVertex(T.label, "person", "name", "target");
+ Edge edge = source.addEdge("rated", target, "name", "before");
+ commitTx();
+ Object id = edge.id();
+ edge = graph().traversal().V(target.id()).inE("rated").next();
+ edge.property("name", "after");
+ edge.remove();
+
+ this.assertRemovedEdgeCounts(id, source, target);
+ graph().tx().rollback();
+ Assert.assertEquals(1L, graph().traversal().E(id).count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated")
+ .count().next().longValue());
+ Assert.assertEquals(selfLoop ? 2L : 1L,
+ graph().traversal().V(source.id()).bothE("rated")
+ .count().next().longValue());
+ Assert.assertEquals("before", graph().traversal().E(id).values("name").next());
+ }
+
+ private void assertRemovedEdgeCounts(Object id, Vertex source, Vertex target) {
+ Assert.assertEquals(0L, graph().traversal().E(id).count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().V(source.id()).bothE("rated")
+ .count().next().longValue());
+ Assert.assertTrue(graph().traversal().V(source.id()).bothE("rated")
+ .toList().isEmpty());
+ Assert.assertEquals(0L, graph().traversal().V(source.id()).outE("rated")
+ .count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().V(target.id()).inE("rated")
+ .count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().E().hasLabel("rated")
+ .count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().E().count().next().longValue());
+ Assert.assertTrue(graph().traversal().E().toList().isEmpty());
+ }
+
+ @Test
+ public void testDirtyVertexIndexCountRemainsUnsupported() {
+ this.initSchema();
+ graph().schema().indexLabel("personByName").onV("person")
+ .by("name").secondary().create();
+ Vertex vertex = graph().addVertex(T.label, "person", "name", "before");
+ commitTx();
+ vertex = graph().traversal().V(vertex.id()).next();
+ vertex.property("name", "after");
+
+ Assert.assertEquals(1L, graph().traversal().V(vertex.id())
+ .count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().V().count().next().longValue());
+ assertDirtyIndexCountUnsupported(graph().traversal().V()
+ .hasLabel("person").has("name", "before").count());
+ this.assertDirtyIndexLabelCount(graph().traversal().V()
+ .hasLabel("person").count());
+ }
+
+ @Test
+ public void testDirtyEdgeIndexCountRemainsUnsupported() {
+ this.initSchema();
+ graph().schema().edgeLabel("rated").link("person", "person")
+ .properties("name").create();
+ graph().schema().indexLabel("ratedByName").onE("rated")
+ .by("name").secondary().create();
+ Vertex source = graph().addVertex(T.label, "person", "name", "source");
+ Vertex target = graph().addVertex(T.label, "person", "name", "target");
+ Edge edge = source.addEdge("rated", target, "name", "before");
+ commitTx();
+ edge = graph().traversal().E(edge.id()).next();
+ edge.property("name", "after");
+
+ Assert.assertEquals(1L, graph().traversal().E(edge.id())
+ .count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().E().count().next().longValue());
+ assertDirtyIndexCountUnsupported(graph().traversal().E()
+ .hasLabel("rated").has("name", "before").count());
+ this.assertDirtyIndexLabelCount(graph().traversal().E()
+ .hasLabel("rated").count());
+ }
+
+ private void assertDirtyIndexLabelCount(GraphTraversal<?, Long> traversal) {
+ if (storeFeatures().supportsQueryByLabel()) {
+ Assert.assertEquals(1L, traversal.next().longValue());
+ } else {
+ assertDirtyIndexCountUnsupported(traversal);
+ }
+ }
+
+ private static void assertDirtyIndexCountUnsupported(GraphTraversal<?, Long> traversal) {
+ Assert.assertThrows(HugeException.class, traversal::next, e -> {
+ Assert.assertContains("Can't do index query when there are " +
+ "changes in transaction", e.getMessage());
+ });
+ }
+
+ @Test
+ public void testRemovedVertexCanBeReaddedWithSameId() {
+ this.initSchema();
+ graph().schema().vertexLabel("reused").properties("name")
+ .useCustomizeStringId().create();
+ Vertex vertex = graph().addVertex(T.label, "reused", T.id, "same",
+ "name", "before");
+ commitTx();
+ vertex = graph().traversal().V(vertex.id()).next();
+ vertex.property("name", "changed");
+ vertex.remove();
+ Vertex replacement = graph().addVertex(T.label, "reused", T.id, "same",
+ "name", "again");
+
+ Assert.assertEquals(1L, graph().traversal().V(replacement.id())
+ .count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().V().hasLabel("reused")
+ .count().next().longValue());
+ Assert.assertEquals(Collections.singletonList(replacement),
+ graph().traversal().V().toList());
+ Assert.assertEquals("again", graph().traversal().V(replacement.id())
+ .values("name").next());
+ }
+
+ @Test
+ public void testReaddedVertexShadowsUpdatedVertexBeforeLabelMatching() {
+ SchemaManager schema = graph().schema();
+ schema.propertyKey("name").asText().create();
+ schema.vertexLabel("person").properties("name")
+ .useCustomizeStringId().create();
+ schema.vertexLabel("software").properties("name")
+ .useCustomizeStringId().create();
+ Vertex vertex = graph().addVertex(T.label, "person", T.id, "same",
+ "name", "before");
+ commitTx();
+ Object id = vertex.id();
+ vertex = graph().traversal().V(id).next();
+ vertex.property("name", "after");
+ vertex.remove();
+ Vertex replacement = graph().addVertex(T.label, "software", T.id, "same",
+ "name", "again");
+
+ Assert.assertEquals(0L, graph().traversal().V(id).hasLabel("person")
+ .count().next().longValue());
+ Assert.assertTrue(graph().traversal().V(id).hasLabel("person")
+ .toList().isEmpty());
+ Assert.assertEquals(0L, graph().traversal().V().hasLabel("person")
+ .count().next().longValue());
+ Assert.assertTrue(graph().traversal().V().hasLabel("person")
+ .toList().isEmpty());
+ Assert.assertEquals(1L, graph().traversal().V(id).hasLabel("software")
+ .count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().V().hasLabel("software")
+ .count().next().longValue());
+ Assert.assertEquals(Collections.singletonList(replacement),
+ graph().traversal().V().hasLabel("software").toList());
+ Assert.assertEquals(Collections.singletonList(replacement),
+ graph().traversal().V(id).toList());
+ Assert.assertEquals(Collections.singletonList(replacement),
+ graph().traversal().V().toList());
+ Assert.assertEquals(1L, graph().traversal().V().count().next().longValue());
+ Assert.assertEquals("again", graph().traversal().V(id).values("name").next());
+
+ graph().tx().rollback();
+ Assert.assertEquals(1L, graph().traversal().V().hasLabel("person")
+ .count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().V().hasLabel("software")
+ .count().next().longValue());
+ Assert.assertEquals("before", graph().traversal().V(id).values("name").next());
+ }
+
+ @Test
+ public void testRemovedEdgeCanBeReaddedWithSameId() {
+ this.initSchema();
+ graph().schema().edgeLabel("rated").link("person", "person")
+ .properties("name").create();
+ Vertex source = graph().addVertex(T.label, "person", "name", "source");
+ Vertex target = graph().addVertex(T.label, "person", "name", "target");
+ Edge edge = source.addEdge("rated", target, "name", "before");
+ commitTx();
+ edge = graph().traversal().E(edge.id()).next();
+ edge.property("name", "changed");
+ edge.remove();
+ Edge replacement = source.addEdge("rated", target, "name", "again");
+ Assert.assertEquals(edge.id(), replacement.id());
+ Assert.assertEquals(1L, graph().traversal().E(replacement.id())
+ .count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated")
+ .count().next().longValue());
+ Assert.assertEquals(Collections.singletonList(replacement),
+ graph().traversal().E().toList());
+ Assert.assertEquals("again", graph().traversal().E(replacement.id())
+ .values("name").next());
+ Assert.assertTrue(graph().traversal().E().hasLabel("rated")
+ .has("name", TextP.containing("changed"))
+ .toList().isEmpty());
+ Assert.assertEquals(0L, graph().traversal().E().hasLabel("rated")
+ .has("name", TextP.containing("changed"))
+ .count().next().longValue());
+ Assert.assertEquals(0L, graph().traversal().E(replacement.id())
+ .has("name", TextP.containing("changed"))
+ .count().next().longValue());
+ Assert.assertEquals(Collections.singletonList(replacement),
+ graph().traversal().E().hasLabel("rated")
+ .has("name", TextP.containing("again")).toList());
+ Assert.assertEquals(1L, graph().traversal().E().hasLabel("rated")
+ .has("name", TextP.containing("again"))
+ .count().next().longValue());
+
+ graph().tx().rollback();
+ Assert.assertEquals(1L, graph().traversal().E(replacement.id())
+ .count().next().longValue());
+ Assert.assertEquals("before", graph().traversal().E(replacement.id())
+ .values("name").next());
+ }
+
+ @Test
+ public void testAddedSelfLoopCountKeepsBothDirections() {
+ this.initSchema();
+ Vertex vertex = graph().addVertex(T.label, "person", "name", "loop");
+ vertex.addEdge("knows", vertex);
+ this.assertSelfLoopCounts(vertex);
+ commitTx();
+ this.assertSelfLoopCounts(vertex);
+ }
+
+ @Test
+ public void testUpdatedSelfLoopCountKeepsBothDirections() {
+ this.initSchema();
+ graph().schema().edgeLabel("rated").link("person", "person")
+ .properties("name").create();
+ Vertex vertex = graph().addVertex(T.label, "person", "name", "loop");
+ Edge loop = vertex.addEdge("rated", vertex, "name", "before");
+ commitTx();
+ loop = graph().traversal().E(loop.id()).next();
+ loop.property("name", "after");
+ this.assertSelfLoopCounts(vertex);
+ commitTx();
+ this.assertSelfLoopCounts(vertex);
+ loop.remove();
+ Assert.assertEquals(0L, graph().traversal().V(vertex.id())
+ .bothE().count().next().longValue());
+ }
+
+ private void assertSelfLoopCounts(Vertex vertex) {
+ Assert.assertEquals(2L, graph().traversal().V(vertex.id())
+ .bothE().count().next().longValue());
+ Assert.assertEquals(2, graph().traversal().V(vertex.id())
+ .bothE().toList().size());
+ Assert.assertEquals(1L, graph().traversal().V(vertex.id())
+ .outE().count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().V(vertex.id())
+ .inE().count().next().longValue());
+ Assert.assertEquals(1L, graph().traversal().E().count().next().longValue());
+ }
+
+ @Test
+ public void testOptimizedEdgeCountIncludesUncommittedRecords() {
+ this.initSchema();
+ graph().schema().indexLabel("personByName")
+ .onV("person").by("name").create();
+ this.initGraph();
+
+ Vertex josh = graph().traversal().V()
+ .hasLabel("person").has("name", "josh").next();
+ Vertex marko = graph().traversal().V()
+ .hasLabel("person").has("name", "marko").next();
+ josh.addEdge("knows", marko);
+
+ long count = graph().traversal().E().hasLabel("knows").count().next();
+
+ Assert.assertEquals(2L, count);
+ }
+
+ private static Query countQuery(HugeType type) {
+ Query query = new Query(type);
+ query.aggregate(new Aggregate(AggregateFunc.COUNT, null));
+ return query;
+ }
+
+ private GraphTransaction newCountTransaction(
+ Iterator<Vertex> vertices, Iterator<Edge> edges,
+ AtomicBoolean dirty) {
+ return new GraphTransaction(params(), params().loadGraphStore()) {
+
+ @Override
+ public boolean hasUpdate() {
+ return dirty.get();
+ }
+
+ @Override
+ public Iterator<Vertex> queryVertices(Query query) {
+ return vertices;
+ }
+
+ @Override
+ public Iterator<Edge> queryEdges(Query query) {
+ return edges;
+ }
+ };
+ }
+
+ private static final class CountCloseableIterator<T>
+ implements CloseableIterator<T> {
+
+ private final boolean fail;
+ private boolean consumed;
+ private boolean closed;
+
+ private CountCloseableIterator(boolean fail) {
+ this.fail = fail;
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (this.fail) {
+ throw new IllegalStateException("Injected iterator failure");
+ }
+ return !this.consumed;
+ }
+
+ @Override
+ public T next() {
+ if (!this.hasNext()) {
+ throw new NoSuchElementException();
+ }
+ this.consumed = true;
+ return null;
+ }
+
+ @Override
+ public void close() {
+ this.closed = true;
+ }
+
+ public boolean closed() {
+ return this.closed;
+ }
+ }
+
+ @Test
+ public void testEdgeRangeCountRejectsUncommittedAddition() {
+ this.initSchema();
+ graph().schema().indexLabel("personByName")
+ .onV("person").by("name").create();
+ this.initGraph();
+ Vertex josh = graph().traversal().V()
+ .hasLabel("person")
+ .has("name", "josh")
+ .next();
+ Vertex marko = graph().traversal().V()
+ .hasLabel("person")
+ .has("name", "marko")
+ .next();
+ josh.addEdge("knows", marko);
+
+ assertUncommittedRangeUnsupported(
+ graph().traversal().E().range(1L, 3L).count());
+ }
+
+ @Test
+ public void testEdgeLimitCountRejectsUncommittedDeletion() {
+ this.initSchema();
+ this.initGraph();
+ Edge edge = graph().traversal().E().hasLabel("knows").next();
+ edge.remove();
+
+ assertUncommittedRangeUnsupported(
+ graph().traversal().E().limit(1L).count());
+ }
+
@Test
public void testRepeatAfterTextRangeFilterWithEmptyResult() {
this.initTextRangeSchema(true);
diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
index c79db5056..38749ce7b 100644
--- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
+++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java
@@ -17,7 +17,11 @@
package org.apache.hugegraph.traversal.optimize;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import org.apache.hugegraph.HugeGraph;
@@ -29,10 +33,12 @@ import org.apache.hugegraph.testutil.Assert;
import org.apache.hugegraph.type.define.DataType;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Step;
+import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep;
@@ -122,6 +128,31 @@ public class TraversalUtilOptimizeTest {
Assert.assertTrue(hasStepExists(traversal));
}
+ @Test
+ public void testExtractHasContainerKeepsMixedTextGraphHasStepLocal() {
+ HugeGraph graph = Mockito.mock(HugeGraph.class);
+ PropertyKey age = propertyKey(1L, "age", DataType.INT);
+ PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+ Mockito.when(graph.propertyKey("age")).thenReturn(age);
+ Mockito.when(graph.propertyKey("name")).thenReturn(name);
+
+ Traversal.Admin<?, ?> traversal = traversal(
+ __.V().has("person", "name", TextP.containing("ar")),
+ graph);
+ HasStep<?> hasStep = (HasStep<?>) traversal.getEndStep();
+ hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+ HugeGraphStep<?, ?> newStep = replaceGraphStep(traversal);
+
+ TraversalUtil.extractHasContainer(newStep, traversal);
+
+ Assert.assertFalse(hasContainer(newStep, T.label.getAccessor()));
+ Assert.assertFalse(hasContainer(newStep, "age"));
+ Assert.assertFalse(hasContainer(newStep, "name"));
+ Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor()));
+ Assert.assertTrue(hasStepExists(traversal, "age"));
+ Assert.assertTrue(hasStepExists(traversal, "name"));
+ }
+
@Test
public void testExtractHasContainerKeepsTextRangeWithoutGraph() {
Traversal.Admin<?, ?> traversal = __.V()
@@ -280,6 +311,31 @@ public class TraversalUtilOptimizeTest {
Assert.assertTrue(hasStepExists(traversal));
}
+ @Test
+ public void testExtractHasContainerKeepsMixedTextVertexHasStepLocal() {
+ HugeGraph graph = Mockito.mock(HugeGraph.class);
+ PropertyKey age = propertyKey(1L, "age", DataType.INT);
+ PropertyKey name = propertyKey(2L, "name", DataType.TEXT);
+ Mockito.when(graph.propertyKey("age")).thenReturn(age);
+ Mockito.when(graph.propertyKey("name")).thenReturn(name);
+
+ Traversal.Admin<?, ?> traversal = traversal(
+ __.V().out().has("person", "name", TextP.containing("ar")),
+ graph);
+ HasStep<?> hasStep = (HasStep<?>) traversal.getEndStep();
+ hasStep.addHasContainer(new HasContainer("age", P.eq(29)));
+ HugeVertexStep<?> newStep = replaceVertexStep(traversal);
+
+ TraversalUtil.extractHasContainer(newStep, traversal);
+
+ Assert.assertFalse(hasContainer(newStep, T.label.getAccessor()));
+ Assert.assertFalse(hasContainer(newStep, "age"));
+ Assert.assertFalse(hasContainer(newStep, "name"));
+ Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor()));
+ Assert.assertTrue(hasStepExists(traversal, "age"));
+ Assert.assertTrue(hasStepExists(traversal, "name"));
+ }
+
@Test
public void testExtractHasContainerRemovesSafeVertexHasStep() {
HugeGraph graph = Mockito.mock(HugeGraph.class);
@@ -296,6 +352,40 @@ public class TraversalUtilOptimizeTest {
Assert.assertFalse(hasStepExists(traversal));
}
+ @Test
+ public void testLabelContainersWithImmutableList() {
+ assertNullHostileLabelCollection(List.of("person"));
+ }
+
+ @Test
+ public void testLabelContainersWithImmutableSet() {
+ assertNullHostileLabelCollection(Set.of("person"));
+ }
+
+ private static void assertNullHostileLabelCollection(Collection<String> labels) {
+ HasContainer within = new HasContainer(T.label.getAccessor(), P.within(labels));
+ Assert.assertTrue(TraversalUtil.canExtractHasContainer(null, within));
+ Assert.assertTrue(TraversalUtil.isPositiveLabelContainer(within));
+ HasContainer without = new HasContainer(T.label.getAccessor(), P.without(labels));
+ Assert.assertTrue(TraversalUtil.canExtractHasContainer(null, without));
+ Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(without));
+ }
+
+ @Test
+ public void testLabelContainersWithMutableNullValuesStayLocal() {
+ Collection<String> list = new ArrayList<>(List.of("person"));
+ list.add(null);
+ Collection<String> set = new HashSet<>(list);
+ for (Collection<String> labels : List.of(list, set)) {
+ HasContainer within = new HasContainer(T.label.getAccessor(), P.within(labels));
+ Assert.assertFalse(TraversalUtil.canExtractHasContainer(null, within));
+ Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(within));
+ HasContainer without = new HasContainer(T.label.getAccessor(), P.without(labels));
+ Assert.assertFalse(TraversalUtil.canExtractHasContainer(null, without));
+ Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(without));
+ }
+ }
+
@Test
public void testIsPositiveLabelContainer() {
Assert.assertTrue(TraversalUtil.isPositiveLabelContainer(
@@ -314,6 +404,11 @@ public class TraversalUtilOptimizeTest {
Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
new HasContainer(T.label.getAccessor(),
P.within(Collections.emptyList()))));
+ Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
+ new HasContainer(T.label.getAccessor(), P.eq(null))));
+ Assert.assertFalse(TraversalUtil.isPositiveLabelContainer(
+ new HasContainer(T.label.getAccessor(),
+ P.within(null, "person"))));
}
@Test
@@ -419,7 +514,8 @@ public class TraversalUtilOptimizeTest {
TraversalHelper.replaceStep((Step) origin, (Step) newStep, traversal);
}
- private static boolean hasContainer(HugeGraphStep<?, ?> step, String key) {
+ private static boolean hasContainer(HasContainerHolder step,
+ String key) {
for (HasContainer has : step.getHasContainers()) {
if (key.equals(has.getKey())) {
return true;