This is an automated email from the ASF dual-hosted git repository.

jin 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 9336b5e29 fix(server): guard count strategy on negative bounds (#2993)
9336b5e29 is described below

commit 9336b5e298511a40c0f23916d86f195cf5f8e2dc
Author: contrueCT <[email protected]>
AuthorDate: Fri Apr 17 15:51:02 2026 +0800

    fix(server): guard count strategy on negative bounds (#2993)
    
    * fix(traversal): guard count strategy on negative bounds
    
    * test(traversal): cover repeat count on negative bounds
    
    * test(traversal): cover collection count on negative bounds
---
 .../main/java/org/apache/hugegraph/HugeGraph.java  |   6 +-
 .../traversal/optimize/HugeCountStrategy.java      | 272 +++++++++++++++++++++
 .../org/apache/hugegraph/core/CoreTestSuite.java   |   1 +
 .../hugegraph/core/CountStrategyCoreTest.java      | 128 ++++++++++
 4 files changed, 406 insertions(+), 1 deletion(-)

diff --git 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
index 88f1142e9..deaa458c2 100644
--- 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
+++ 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java
@@ -49,6 +49,7 @@ import org.apache.hugegraph.schema.SchemaManager;
 import org.apache.hugegraph.schema.VertexLabel;
 import org.apache.hugegraph.structure.HugeFeatures;
 import org.apache.hugegraph.task.TaskScheduler;
+import org.apache.hugegraph.traversal.optimize.HugeCountStrategy;
 import org.apache.hugegraph.traversal.optimize.HugeCountStepStrategy;
 import org.apache.hugegraph.traversal.optimize.HugeGraphStepStrategy;
 import org.apache.hugegraph.traversal.optimize.HugePrimaryKeyStrategy;
@@ -57,6 +58,7 @@ import org.apache.hugegraph.type.HugeType;
 import org.apache.hugegraph.type.define.GraphMode;
 import org.apache.hugegraph.type.define.GraphReadMode;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.CountStrategy;
 import org.apache.tinkerpop.gremlin.structure.Edge;
 import org.apache.tinkerpop.gremlin.structure.Graph;
 import org.apache.tinkerpop.gremlin.structure.Property;
@@ -378,7 +380,9 @@ public interface HugeGraph extends Graph {
         TraversalStrategies strategies = TraversalStrategies.GlobalCache
                 .getStrategies(Graph.class)
                 .clone();
-        strategies.addStrategies(HugeVertexStepStrategy.instance(),
+        strategies.removeStrategies(CountStrategy.class);
+        strategies.addStrategies(HugeCountStrategy.instance(),
+                                 HugeVertexStepStrategy.instance(),
                                  HugeGraphStepStrategy.instance(),
                                  HugeCountStepStrategy.instance(),
                                  HugePrimaryKeyStrategy.instance());
diff --git 
a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java
 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java
new file mode 100644
index 000000000..5a8c468a4
--- /dev/null
+++ 
b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/optimize/HugeCountStrategy.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.hugegraph.traversal.optimize;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiPredicate;
+
+import org.apache.tinkerpop.gremlin.process.traversal.Compare;
+import org.apache.tinkerpop.gremlin.process.traversal.Contains;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Step;
+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.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
+import org.apache.tinkerpop.gremlin.process.traversal.step.branch.RepeatStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.step.filter.ConnectiveStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NotStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SideEffectStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep;
+import 
org.apache.tinkerpop.gremlin.process.traversal.strategy.AbstractTraversalStrategy;
+import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
+
+/**
+ * HugeGraph keeps a local copy of TinkerPop's CountStrategy so we can
+ * safely patch negative-threshold handling without waiting for a full
+ * dependency upgrade. Count() never yields a negative result, so any
+ * optimization that turns a negative bound into not() or a negative range
+ * changes the traversal semantics.
+ */
+public final class HugeCountStrategy
+        extends 
AbstractTraversalStrategy<TraversalStrategy.OptimizationStrategy>
+        implements TraversalStrategy.OptimizationStrategy {
+
+    private static final Map<BiPredicate, Long> RANGE_PREDICATES =
+            new HashMap<BiPredicate, Long>() {{
+                put(Contains.within, 1L);
+                put(Contains.without, 0L);
+            }};
+    private static final Set<Compare> INCREASED_OFFSET_SCALAR_PREDICATES =
+            EnumSet.of(Compare.eq, Compare.neq, Compare.lte, Compare.gt);
+
+    private static final HugeCountStrategy INSTANCE = new HugeCountStrategy();
+
+    private HugeCountStrategy() {
+    }
+
+    public static HugeCountStrategy instance() {
+        return INSTANCE;
+    }
+
+    @Override
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public void apply(final Traversal.Admin<?, ?> traversal) {
+        final TraversalParent parent = traversal.getParent();
+        int size = traversal.getSteps().size();
+        Step prev = null;
+        for (int i = 0; i < size; i++) {
+            final Step curr = traversal.getSteps().get(i);
+            if (i < size - 1 && doStrategy(curr)) {
+                final IsStep isStep = (IsStep) traversal.getSteps().get(i + 1);
+                final P isStepPredicate = isStep.getPredicate();
+                Long highRange = null;
+                boolean useNotStep = false;
+                boolean dismissCountIs = false;
+                boolean hasNegativeCompareBound = false;
+
+                for (P p : isStepPredicate instanceof ConnectiveP ?
+                           ((ConnectiveP<?>) isStepPredicate).getPredicates() :
+                           Collections.singletonList(isStepPredicate)) {
+                    final Object value = p.getValue();
+                    final BiPredicate predicate = p.getBiPredicate();
+                    if (value instanceof Number) {
+                        final long highRangeOffset =
+                                
INCREASED_OFFSET_SCALAR_PREDICATES.contains(predicate) ?
+                                1L : 0L;
+                        final Long highRangeCandidate =
+                                (long) Math.ceil(((Number) 
value).doubleValue()) +
+                                highRangeOffset;
+
+                        if ((predicate.equals(Compare.gt) ||
+                             predicate.equals(Compare.gte) ||
+                             predicate.equals(Compare.lt) ||
+                             predicate.equals(Compare.lte)) &&
+                            highRangeCandidate < 1L) {
+                            hasNegativeCompareBound = true;
+                        }
+
+                        final boolean update = highRange == null ||
+                                               highRangeCandidate > highRange;
+                        if (update) {
+                            if (parent instanceof EmptyStep) {
+                                useNotStep = false;
+                            } else if (parent instanceof RepeatStep) {
+                                final RepeatStep repeatStep = (RepeatStep) 
parent;
+                                dismissCountIs = useNotStep =
+                                        Objects.equals(traversal,
+                                                       
repeatStep.getUntilTraversal()) ||
+                                        Objects.equals(traversal,
+                                                       
repeatStep.getEmitTraversal());
+                            } else {
+                                dismissCountIs = useNotStep =
+                                        parent instanceof FilterStep ||
+                                        parent instanceof SideEffectStep;
+                            }
+                            highRange = highRangeCandidate;
+                            useNotStep &= curr.getLabels().isEmpty() &&
+                                          isStep.getLabels().isEmpty() &&
+                                          isStep.getNextStep() instanceof 
EmptyStep &&
+                                          ((highRange <= 1L &&
+                                            predicate.equals(Compare.lt)) ||
+                                           (highRange == 1L &&
+                                            (predicate.equals(Compare.eq) ||
+                                             predicate.equals(Compare.lte))));
+                            dismissCountIs &= curr.getLabels().isEmpty() &&
+                                              isStep.getLabels().isEmpty() &&
+                                              isStep.getNextStep() instanceof 
EmptyStep &&
+                                              (highRange == 1L &&
+                                               (predicate.equals(Compare.gt) ||
+                                                
predicate.equals(Compare.gte)));
+                        }
+                        if (hasNegativeCompareBound) {
+                            /*
+                             * TINKERPOP-2893:
+                             * count() is always >= 0, so optimizations like
+                             * is(lt(-3)) -> not(...) are not semantics-safe.
+                             */
+                            useNotStep = false;
+                            dismissCountIs = false;
+                        }
+                    } else {
+                        final Long highRangeOffset = 
RANGE_PREDICATES.get(predicate);
+                        if (value instanceof Collection && highRangeOffset != 
null) {
+                            final Object high = Collections.max((Collection) 
value);
+                            if (high instanceof Number) {
+                                final Long highRangeCandidate =
+                                        ((Number) high).longValue() + 
highRangeOffset;
+                                final boolean update = highRange == null ||
+                                                       highRangeCandidate > 
highRange;
+                                if (update) {
+                                    highRange = highRangeCandidate;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                /*
+                 * HugeGraph extracts RangeGlobalStep into backend queries. A
+                 * negative upper bound is never useful for count(), and would
+                 * become an invalid backend range like [0, -3).
+                 */
+                if (highRange != null && highRange < 0L) {
+                    highRange = null;
+                }
+
+                if (highRange != null) {
+                    if (useNotStep || dismissCountIs) {
+                        traversal.asAdmin().removeStep(isStep);
+                        traversal.asAdmin().removeStep(curr);
+                        size -= 2;
+                        if (!dismissCountIs) {
+                            final TraversalParent p;
+                            if ((p = traversal.getParent()) instanceof 
FilterStep &&
+                                !(p instanceof ConnectiveStep)) {
+                                final Step<?, ?> filterStep = parent.asStep();
+                                final Traversal.Admin parentTraversal =
+                                        filterStep.getTraversal();
+                                final Step notStep = new NotStep<>(
+                                        parentTraversal,
+                                        traversal.getSteps().isEmpty() ?
+                                        __.identity() : traversal);
+                                
filterStep.getLabels().forEach(notStep::addLabel);
+                                TraversalHelper.replaceStep(filterStep, 
notStep,
+                                                            parentTraversal);
+                            } else {
+                                final Traversal.Admin inner;
+                                if (prev != null) {
+                                    inner = __.start().asAdmin();
+                                    for (;;) {
+                                        final Step pp = prev.getPreviousStep();
+                                        inner.addStep(0, prev);
+                                        if (pp instanceof EmptyStep ||
+                                            pp instanceof GraphStep ||
+                                            !(prev instanceof FilterStep ||
+                                              prev instanceof SideEffectStep)) 
{
+                                            break;
+                                        }
+                                        traversal.removeStep(prev);
+                                        prev = pp;
+                                        size--;
+                                    }
+                                } else {
+                                    inner = __.identity().asAdmin();
+                                }
+                                if (prev != null) {
+                                    TraversalHelper.replaceStep(
+                                            prev,
+                                            new NotStep<>(traversal, inner),
+                                            traversal);
+                                } else {
+                                    traversal.asAdmin().addStep(
+                                            new NotStep<>(traversal, inner));
+                                }
+                            }
+                        } else if (size == 0) {
+                            final Step parentStep = 
traversal.getParent().asStep();
+                            if (!(parentStep instanceof EmptyStep)) {
+                                final Traversal.Admin parentTraversal =
+                                        parentStep.getTraversal();
+                                TraversalHelper.replaceStep(
+                                        parentStep,
+                                        new IdentityStep<>(parentTraversal),
+                                        parentTraversal);
+                            }
+                        }
+                    } else {
+                        TraversalHelper.insertBeforeStep(
+                                new RangeGlobalStep<>(traversal, 0L, 
highRange),
+                                curr, traversal);
+                    }
+                    i++;
+                }
+            }
+            prev = curr;
+        }
+    }
+
+    private boolean doStrategy(final Step step) {
+        if (!(step instanceof CountGlobalStep) ||
+            !(step.getNextStep() instanceof IsStep) ||
+            step.getPreviousStep() instanceof RangeGlobalStep) {
+            return false;
+        }
+
+        final Step parent = step.getTraversal().getParent().asStep();
+        return (parent instanceof FilterStep || parent.getLabels().isEmpty()) 
&&
+               !(parent.getNextStep() instanceof MatchStep.MatchEndStep &&
+                 ((MatchStep.MatchEndStep) parent.getNextStep())
+                         .getMatchKey().isPresent());
+    }
+}
diff --git 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
index 1f870208c..e78aff7e2 100644
--- 
a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java
@@ -39,6 +39,7 @@ import org.slf4j.Logger;
         IndexLabelCoreTest.class,
         VertexCoreTest.class,
         EdgeCoreTest.class,
+        CountStrategyCoreTest.class,
         ParentAndSubEdgeCoreTest.class,
         PropertyCoreTest.VertexPropertyCoreTest.class,
         PropertyCoreTest.EdgePropertyCoreTest.class,
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
new file mode 100644
index 000000000..fdfc9d2e4
--- /dev/null
+++ 
b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CountStrategyCoreTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hugegraph.core;
+
+import org.apache.hugegraph.schema.SchemaManager;
+import org.apache.hugegraph.testutil.Assert;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.structure.T;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.junit.Test;
+
+public class CountStrategyCoreTest extends BaseCoreTest {
+
+    private void initSchema() {
+        SchemaManager schema = graph().schema();
+        schema.propertyKey("name").asText().create();
+        schema.vertexLabel("person").properties("name")
+              .nullableKeys("name").create();
+        schema.vertexLabel("software").properties("name")
+              .nullableKeys("name").create();
+        schema.edgeLabel("knows").link("person", "person").create();
+        schema.edgeLabel("created").link("person", "software").create();
+    }
+
+    private void initGraph() {
+        Vertex marko = graph().addVertex(T.label, "person", "name", "marko");
+        Vertex josh = graph().addVertex(T.label, "person", "name", "josh");
+        Vertex lop = graph().addVertex(T.label, "software", "name", "lop");
+
+        marko.addEdge("knows", josh);
+        marko.addEdge("created", lop);
+        commitTx();
+    }
+
+    @Test
+    public void testWhereCountLtNegativeIsAlwaysFalse() {
+        this.initSchema();
+        this.initGraph();
+
+        long count = graph().traversal().E().outV()
+                            .repeat(__.both()).times(1)
+                            .where(__.outE().count().is(P.lt(-3)))
+                            .count().next();
+
+        Assert.assertEquals(0L, count);
+    }
+
+    @Test
+    public void testWhereCountOutsideNegativeKeepsOriginalSemantics() {
+        this.initSchema();
+        this.initGraph();
+
+        long direct = graph().traversal().V()
+                           .both("created")
+                           .inE("created")
+                           .where(__.bothV().count().is(P.outside(-3, -5)))
+                           .count().next();
+        long viaMatch = graph().traversal().V()
+                             .repeat(__.both("created")).times(1)
+                             .inE("created")
+                             .match(__.as("start")
+                                      .where(__.bothV().count()
+                                               .is(P.outside(-3, -5)))
+                                      .as("end"))
+                             .select("end")
+                             .count().next();
+
+        Assert.assertEquals(1L, direct);
+        Assert.assertEquals(viaMatch, direct);
+    }
+
+    @Test
+    public void testRepeatUntilCountLtNegativeIsAlwaysFalse() {
+        this.initSchema();
+        this.initGraph();
+
+        long count = graph().traversal().E()
+                            .hasLabel("knows")
+                            .outV()
+                            .repeat(__.out())
+                            .until(__.outE().count().is(P.lt(-1)))
+                            .count().next();
+
+        Assert.assertEquals(0L, count);
+    }
+
+    @Test
+    public void testWhereCountWithinNegativeCollectionIsAlwaysFalse() {
+        this.initSchema();
+        this.initGraph();
+
+        long count = graph().traversal().V()
+                            .where(__.outE().count().is(P.within(-3, -5)))
+                            .count().next();
+
+        Assert.assertEquals(0L, count);
+    }
+
+    @Test
+    public void testWhereCountGteNegativeDoesNotBuildInvalidRange() {
+        this.initSchema();
+        this.initGraph();
+
+        long count = graph().traversal().E()
+                            .bothV()
+                            .where(__.out("knows", "created")
+                                     .count().is(P.gte(-3)))
+                            .count().next();
+
+        Assert.assertEquals(4L, count);
+    }
+}

Reply via email to