From ac25ec585d4914dffd7358ea3a1374eb613fe30f Mon Sep 17 00:00:00 2001 From: contrueCT Date: Sun, 12 Apr 2026 21:39:46 +0800 Subject: [PATCH 01/11] improve(query): clarify condition resolution semantics Add explicit condition resolution APIs to ConditionQuery while preserving the legacy condition() behavior. Introduce containsCondition(Object), conditionValues(Object), and conditionValue(Object) so callers can distinguish missing, empty, unique, and multi-value results without overloading null semantics. Migrate LABEL-specific consumers in graph/index transactions, serializers, traversers, and stores to use the new APIs for unique-label resolution and conservative fallback behavior. Extend QueryTest and VertexCoreTest to cover absent, conflicting, and multi-value label conditions as well as collectMatchedIndexes() behavior for multi-label and conflicting label queries. --- .../backend/query/ConditionQuery.java | 124 +++++++++++++++--- .../backend/serializer/BinarySerializer.java | 5 +- .../backend/serializer/TextSerializer.java | 5 +- .../hugegraph/backend/store/ram/RamTable.java | 12 +- .../backend/tx/GraphIndexTransaction.java | 31 +++-- .../backend/tx/GraphTransaction.java | 16 ++- .../traversal/algorithm/HugeTraverser.java | 2 +- .../backend/store/hstore/HstoreStore.java | 14 +- .../apache/hugegraph/core/VertexCoreTest.java | 44 +++++++ .../apache/hugegraph/unit/core/QueryTest.java | 46 +++++++ 10 files changed, 261 insertions(+), 38 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java index 063d23aa6d..5b37622514 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java @@ -256,24 +256,26 @@ public boolean containsLabelOrUserpropRelation() { return false; } + /** + * Returns the legacy condition value of the specified key. + * + * This method keeps the historical behavior for existing callers: + * + * + * Prefer {@link #conditionValues(Object)} or {@link #conditionValue(Object)} + * for new code that needs explicit semantics. + */ @Watched public T condition(Object key) { List valuesEQ = InsertionOrderUtil.newList(); List valuesIN = InsertionOrderUtil.newList(); - for (Condition c : this.conditions) { - if (c.isRelation()) { - Condition.Relation r = (Condition.Relation) c; - if (r.key().equals(key)) { - if (r.relation() == RelationType.EQ) { - valuesEQ.add(r.value()); - } else if (r.relation() == RelationType.IN) { - Object value = r.value(); - assert value instanceof List; - valuesIN.add(value); - } - } - } - } + this.collectConditionValues(key, valuesEQ, valuesIN); if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { return null; } @@ -323,20 +325,110 @@ public T condition(Object key) { return value; } + /** + * Returns whether there is any top-level relation for the specified key. + */ + public boolean containsCondition(Object key) { + for (Condition c : this.conditions) { + if (c.isRelation()) { + Condition.Relation r = (Condition.Relation) c; + if (r.key().equals(key)) { + return true; + } + } + } + return false; + } + + /** + * Returns the resolved candidate values of the specified key from + * top-level EQ/IN relations. + * + * Use {@link #containsCondition(Object)} to distinguish "no condition" + * from "conditions exist but resolve to an empty intersection". + */ + public Set conditionValues(Object key) { + List valuesEQ = InsertionOrderUtil.newList(); + List valuesIN = InsertionOrderUtil.newList(); + this.collectConditionValues(key, valuesEQ, valuesIN); + if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { + return InsertionOrderUtil.newSet(); + } + return this.resolveConditionValues(valuesEQ, valuesIN); + } + + /** + * Returns the unique resolved value of the specified key from top-level + * EQ/IN relations. + * + * Returns {@code null} when the resolved candidate set is empty. Throws + * if multiple values remain after resolution. + */ + public T conditionValue(Object key) { + Set values = this.conditionValues(key); + if (values.isEmpty()) { + return null; + } + E.checkState(values.size() == 1, + "Illegal key '%s' with more than one value: %s", + key, values); + @SuppressWarnings("unchecked") + T value = (T) values.iterator().next(); + return value; + } + public void unsetCondition(Object key) { this.conditions.removeIf(c -> c.isRelation() && ((Relation) c).key().equals(key)); } public boolean containsCondition(HugeKeys key) { + return this.containsCondition((Object) key); + } + + private void collectConditionValues(Object key, List valuesEQ, + List valuesIN) { for (Condition c : this.conditions) { if (c.isRelation()) { Condition.Relation r = (Condition.Relation) c; if (r.key().equals(key)) { - return true; + if (r.relation() == RelationType.EQ) { + valuesEQ.add(r.value()); + } else if (r.relation() == RelationType.IN) { + Object value = r.value(); + assert value instanceof List; + valuesIN.add(value); + } } } } - return false; + } + + private Set resolveConditionValues(List valuesEQ, + List valuesIN) { + boolean initialized = false; + Set intersectValues = InsertionOrderUtil.newSet(); + for (Object value : valuesEQ) { + List valueAsList = ImmutableList.of(value); + if (!initialized) { + intersectValues.addAll(valueAsList); + initialized = true; + } else { + CollectionUtil.intersectWithModify(intersectValues, + valueAsList); + } + } + for (Object value : valuesIN) { + @SuppressWarnings("unchecked") + List valueAsList = (List) value; + if (!initialized) { + intersectValues.addAll(valueAsList); + initialized = true; + } else { + CollectionUtil.intersectWithModify(intersectValues, + valueAsList); + } + } + return intersectValues; } public boolean containsCondition(Condition.RelationType type) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java index 0bb07760a5..6f1eec58a6 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java @@ -674,7 +674,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) { if (direction == null) { direction = Directions.OUT; } - Id label = cq.condition(HugeKeys.LABEL); + Id label = cq.conditionValue(HugeKeys.LABEL); BytesBuffer start = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); writePartitionedId(HugeType.EDGE, vertex, start); @@ -722,7 +722,8 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { int count = 0; BytesBuffer buffer = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); for (HugeKeys key : EdgeId.KEYS) { - Object value = cq.condition(key); + Object value = key == HugeKeys.LABEL ? + cq.conditionValue(key) : cq.condition(key); if (value != null) { count++; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java index 2d5cb81ec1..61830b3c54 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java @@ -457,7 +457,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) { if (direction == null) { direction = Directions.OUT; } - Object label = cq.condition(HugeKeys.LABEL); + Object label = cq.conditionValue(HugeKeys.LABEL); List start = new ArrayList<>(cq.conditionsSize()); start.add(writeEntryId((Id) vertex)); @@ -491,7 +491,8 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { List condParts = new ArrayList<>(cq.conditionsSize()); for (HugeKeys key : EdgeId.KEYS) { - Object value = cq.condition(key); + Object value = key == HugeKeys.LABEL ? + cq.conditionValue(key) : cq.condition(key); if (value == null) { break; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java index 0e2c58bddc..c282fe384b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java @@ -269,7 +269,7 @@ public boolean matched(Query query) { int conditionsSize = cq.conditionsSize(); Object owner = cq.condition(HugeKeys.OWNER_VERTEX); Directions direction = cq.condition(HugeKeys.DIRECTION); - Id label = cq.condition(HugeKeys.LABEL); + Id label = uniqueLabel(cq); if (direction == null && conditionsSize > 1) { for (Condition cond : cq.conditions()) { @@ -316,7 +316,7 @@ private Iterator query(ConditionQuery query) { if (dir == null) { dir = Directions.BOTH; } - Id label = query.condition(HugeKeys.LABEL); + Id label = uniqueLabel(query); if (label == null) { label = IdGenerator.ZERO; } @@ -377,6 +377,14 @@ private static void ensureNumberId(Id id) { } } + private static Id uniqueLabel(ConditionQuery query) { + java.util.Set labels = query.conditionValues(HugeKeys.LABEL); + if (labels.size() != 1) { + return null; + } + return (Id) labels.iterator().next(); + } + private static long encode(long target, Directions direction, int label) { // TODO: support property assert (label & 0x0fffffff) == label; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 7388425167..6b358b2619 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -415,8 +415,9 @@ private IdHolderList queryByLabel(ConditionQuery query) { HugeType queryType = query.resultType(); IndexLabel il = IndexLabel.label(queryType); validateIndexLabel(il); - Id label = query.condition(HugeKeys.LABEL); - assert label != null; + Id label = query.conditionValue(HugeKeys.LABEL); + E.checkState(label != null, "Expect one label value for query: %s", + query); HugeType indexType; SchemaLabel schemaLabel; @@ -482,7 +483,7 @@ private IdHolderList queryByUserprop(ConditionQuery query) { } Set indexes = this.collectMatchedIndexes(query); if (indexes.isEmpty()) { - Id label = query.condition(HugeKeys.LABEL); + Id label = uniqueLabel(query); throw noIndexException(this.graph(), query, label); } @@ -756,11 +757,16 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, @Watched(prefix = "index") private Set collectMatchedIndexes(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); - Id label = query.condition(HugeKeys.LABEL); + boolean hasLabel = query.containsCondition(HugeKeys.LABEL); + Set labels = query.conditionValues(HugeKeys.LABEL); List schemaLabels; - if (label != null) { - // Query has LABEL condition + if (hasLabel && labels.isEmpty()) { + return Collections.emptySet(); + } + if (labels.size() == 1) { + Id label = (Id) labels.iterator().next(); + // Query has one resolved LABEL condition SchemaLabel schemaLabel; if (query.resultType().isVertex()) { schemaLabel = schema.getVertexLabel(label); @@ -773,7 +779,8 @@ private Set collectMatchedIndexes(ConditionQuery query) { } schemaLabels = ImmutableList.of(schemaLabel); } else { - // Query doesn't have LABEL condition + // Query doesn't have LABEL condition or it doesn't resolve + // to a single label, so keep the conservative fallback. if (query.resultType().isVertex()) { schemaLabels = schema.getVertexLabels(); } else if (query.resultType().isEdge()) { @@ -1781,7 +1788,7 @@ protected long removeIndexLeft(ConditionQuery query, } // Check label is matched - Id label = query.condition(HugeKeys.LABEL); + Id label = uniqueLabel(query); // NOTE: original condition query may not have label condition, // which means possibly label == null. if (label != null && !element.schemaLabel().id().equals(label)) { @@ -1981,4 +1988,12 @@ public Long reduce(Long t1, Long t2) { return t1 + t2; } } + + private static Id uniqueLabel(ConditionQuery query) { + Set labels = query.conditionValues(HugeKeys.LABEL); + if (labels.size() != 1) { + return null; + } + return (Id) labels.iterator().next(); + } } 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 5e33e0b3fc..6c4f174d7d 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 @@ -1059,7 +1059,7 @@ protected Iterator queryEdgesFromBackend(Query query) { ConditionQueryFlatten.flatten((ConditionQuery) query, supportIn).stream(); Stream> edgeIterators = flattenedQueries.map(cq -> { - Id label = cq.condition(HugeKeys.LABEL); + Id label = uniqueLabel(cq); if (this.storeFeatures().supportsFatherAndSubEdgeLabel() && label != null && graph().edgeLabel(label).isFather() && @@ -1389,7 +1389,7 @@ private static boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = uniqueLabel(query); if (label == null) { return false; } @@ -1522,7 +1522,7 @@ private Query optimizeQuery(ConditionQuery query) { throw new HugeException("Not supported querying by id and conditions: %s", query); } - Id label = query.condition(HugeKeys.LABEL); + Id label = uniqueLabel(query); // Optimize vertex query if (label != null && query.resultType().isVertex()) { @@ -1914,7 +1914,7 @@ private boolean rightResultFromIndexQuery(Query query, HugeElement elem) { } ConditionQuery cq = (ConditionQuery) query; - if (cq.condition(HugeKeys.LABEL) != null && cq.resultType().isEdge()) { + if (uniqueLabel(cq) != null && cq.resultType().isEdge()) { if (cq.conditions().size() == 1) { // g.E().hasLabel(xxx) return true; @@ -1966,6 +1966,14 @@ private boolean rightResultFromIndexQuery(Query query, HugeElement elem) { return false; } + private static Id uniqueLabel(ConditionQuery query) { + Set labels = query.conditionValues(HugeKeys.LABEL); + if (labels.size() != 1) { + return null; + } + return (Id) labels.iterator().next(); + } + private Iterator filterExpiredResultFromBackend( Query query, Iterator results) { if (this.store().features().supportsTtl() || query.showExpired()) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java index 8122c79080..a19c32a04f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java @@ -582,7 +582,7 @@ private void fillFilterBySortKeys(Query query, Id[] edgeLabels, ConditionQuery condQuery = (ConditionQuery) query; if (!GraphTransaction.matchFullEdgeSortKeys(condQuery, this.graph())) { - Id label = condQuery.condition(HugeKeys.LABEL); + Id label = condQuery.conditionValue(HugeKeys.LABEL); E.checkArgument(false, "The properties %s does not match " + "sort keys of edge label '%s'", this.graph().mapPkId2Name(properties.keySet()), diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java index 6439096674..713511490d 100644 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java @@ -402,8 +402,8 @@ public IdPrefixQuery next() { List queryList = Lists.newArrayList(); if (hugeGraph != null) { for (ConditionQuery conditionQuery : - ConditionQueryFlatten.flatten(cq)) { - Id label = conditionQuery.condition(HugeKeys.LABEL); + ConditionQueryFlatten.flatten(cq)) { + Id label = this.uniqueLabel(conditionQuery); /* Parent type + sortKeys: g.V("V.id").outE("parentLabel") .has("sortKey","value") converted to all subtypes + sortKeys */ if ((this.subEls == null || @@ -455,11 +455,19 @@ public IdPrefixQuery next() { buffer.bytes(), ownerId)); } + private Id uniqueLabel(ConditionQuery query) { + Set labels = query.conditionValues(HugeKeys.LABEL); + if (labels.size() != 1) { + return null; + } + return (Id) labels.iterator().next(); + } + private boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = this.uniqueLabel(query); if (label == null) { return false; } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index d33f9bb07d..24525bcf37 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -54,6 +54,7 @@ import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.schema.SchemaLabel; import org.apache.hugegraph.schema.Userdata; import org.apache.hugegraph.schema.VertexLabel; import org.apache.hugegraph.structure.HugeElement; @@ -9076,6 +9077,49 @@ public void testQueryByJointLabels() { Assert.assertEquals(0, vertices.size()); } + @Test + public void testCollectMatchedIndexesByJointLabelsWithIndexedProperties() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + init5Computers(); + init10Vertices(); + + VertexLabel person = graph.vertexLabel("person"); + VertexLabel computer = graph.vertexLabel("computer"); + PropertyKey city = graph.propertyKey("city"); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(person.id(), computer.id()))); + query.query(Condition.eq(city.id(), "Beijing")); + + Set matchedIndexes = Whitebox.invoke(params().graphTransaction(), + "indexTx", + "collectMatchedIndexes", + query); + Assert.assertEquals(1, matchedIndexes.size()); + Object matchedIndex = matchedIndexes.iterator().next(); + SchemaLabel schemaLabel = Whitebox.getInternalState(matchedIndex, + "schemaLabel"); + Assert.assertEquals("person", schemaLabel.name()); + + ConditionQuery conflicting = new ConditionQuery(HugeType.VERTEX); + conflicting.eq(HugeKeys.LABEL, person.id()); + conflicting.eq(HugeKeys.LABEL, computer.id()); + conflicting.query(Condition.eq(city.id(), "Beijing")); + + Assert.assertTrue(conflicting.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + conflicting.conditionValues(HugeKeys.LABEL)); + + matchedIndexes = Whitebox.invoke(params().graphTransaction(), + "indexTx", + "collectMatchedIndexes", + conflicting); + Assert.assertEquals(0, matchedIndexes.size()); + } + @Test public void testQueryByHasIdEmptyList() { HugeGraph graph = graph(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java index 7d48084dbf..bb2951c8ea 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java @@ -48,6 +48,17 @@ public void testOrderBy() { query.orders()); } + @Test + public void testConditionWithoutLabel() { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + + Assert.assertFalse(query.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + Assert.assertNull(query.condition(HugeKeys.LABEL)); + } + @Test public void testConditionWithEqAndIn() { Id label1 = IdGenerator.of(1); @@ -58,9 +69,33 @@ public void testConditionWithEqAndIn() { query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(label1, label2))); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label1), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertEquals(label1, query.conditionValue(HugeKeys.LABEL)); Assert.assertEquals(label1, query.condition(HugeKeys.LABEL)); } + @Test + public void testConditionWithSingleInValues() { + Id label1 = IdGenerator.of(1); + Id label2 = IdGenerator.of(2); + + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(label1, label2))); + + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label1, label2), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertThrows(IllegalStateException.class, + () -> query.conditionValue(HugeKeys.LABEL), + e -> Assert.assertContains("Illegal key 'LABEL'", + e.getMessage())); + Assert.assertEquals(ImmutableList.of(label1, label2), + query.condition(HugeKeys.LABEL)); + } + @Test public void testConditionWithConflictingEqAndIn() { Id label1 = IdGenerator.of(1); @@ -73,6 +108,10 @@ public void testConditionWithConflictingEqAndIn() { query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(label1, label3))); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); Assert.assertNull(query.condition(HugeKeys.LABEL)); } @@ -89,6 +128,13 @@ public void testConditionWithMultipleMatchedInValues() { query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(label1, label2, label4))); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label1, label2), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertThrows(IllegalStateException.class, + () -> query.conditionValue(HugeKeys.LABEL), + e -> Assert.assertContains("Illegal key 'LABEL'", + e.getMessage())); Assert.assertThrows(IllegalStateException.class, () -> query.condition(HugeKeys.LABEL), e -> Assert.assertContains("Illegal key 'LABEL'", From c4cf3d5e9085aa624dca36239ffa9bc26bf37cfb Mon Sep 17 00:00:00 2001 From: contrueCT Date: Mon, 13 Apr 2026 22:08:29 +0800 Subject: [PATCH 02/11] test(core): avoid unnecessary data setup in label index regression --- .../main/java/org/apache/hugegraph/core/VertexCoreTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index 24525bcf37..adbdddc24c 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -9081,9 +9081,6 @@ public void testQueryByJointLabels() { public void testCollectMatchedIndexesByJointLabelsWithIndexedProperties() { HugeGraph graph = graph(); initPersonIndex(true); - init5Persons(); - init5Computers(); - init10Vertices(); VertexLabel person = graph.vertexLabel("person"); VertexLabel computer = graph.vertexLabel("computer"); From ed3b7886fea5901220850de837ccc8076de1519a Mon Sep 17 00:00:00 2001 From: contrueCT Date: Thu, 23 Apr 2026 11:59:26 +0800 Subject: [PATCH 03/11] improve(query): consolidate unique label resolution --- .../backend/query/ConditionQuery.java | 84 +++++++++++++------ .../backend/serializer/BinarySerializer.java | 16 +++- .../backend/serializer/TextSerializer.java | 16 +++- .../hugegraph/backend/store/ram/RamTable.java | 12 +-- .../backend/tx/GraphIndexTransaction.java | 23 ++--- .../backend/tx/GraphTransaction.java | 17 ++-- .../traversal/algorithm/HugeTraverser.java | 2 + .../backend/store/hstore/HstoreStore.java | 12 +-- .../apache/hugegraph/core/VertexCoreTest.java | 16 ++++ .../apache/hugegraph/unit/core/QueryTest.java | 39 +++++++++ 10 files changed, 158 insertions(+), 79 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java index 5b37622514..226091ed36 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java @@ -268,8 +268,9 @@ public boolean containsLabelOrUserpropRelation() { *
  • throws if multiple values remain after resolving several relations
  • * * - * Prefer {@link #conditionValues(Object)} or {@link #conditionValue(Object)} - * for new code that needs explicit semantics. + * Prefer {@link #conditionValues(Object)}, {@link #uniqueConditionValue(Object)} + * or {@link #conditionValue(Object)} for new code that needs explicit + * semantics. */ @Watched public T condition(Object key) { @@ -290,29 +291,8 @@ public T condition(Object key) { return value; } - boolean initialized = false; - Set intersectValues = InsertionOrderUtil.newSet(); - for (Object value : valuesEQ) { - List valueAsList = ImmutableList.of(value); - if (!initialized) { - intersectValues.addAll(valueAsList); - initialized = true; - } else { - CollectionUtil.intersectWithModify(intersectValues, - valueAsList); - } - } - for (Object value : valuesIN) { - @SuppressWarnings("unchecked") - List valueAsList = (List) value; - if (!initialized) { - intersectValues.addAll(valueAsList); - initialized = true; - } else { - CollectionUtil.intersectWithModify(intersectValues, - valueAsList); - } - } + Set intersectValues = this.resolveConditionValues(valuesEQ, + valuesIN); if (intersectValues.isEmpty()) { return null; @@ -344,8 +324,9 @@ public boolean containsCondition(Object key) { * Returns the resolved candidate values of the specified key from * top-level EQ/IN relations. * - * Use {@link #containsCondition(Object)} to distinguish "no condition" - * from "conditions exist but resolve to an empty intersection". + * Use {@link #containsConditionValues(Object)} to distinguish "no EQ/IN + * condition" from "EQ/IN conditions exist but resolve to an empty + * intersection". */ public Set conditionValues(Object key) { List valuesEQ = InsertionOrderUtil.newList(); @@ -357,6 +338,24 @@ public Set conditionValues(Object key) { return this.resolveConditionValues(valuesEQ, valuesIN); } + /** + * Returns whether there is any top-level EQ/IN relation for the specified + * key. + */ + public boolean containsConditionValues(Object key) { + for (Condition c : this.conditions) { + if (c.isRelation()) { + Condition.Relation r = (Condition.Relation) c; + if (r.key().equals(key) && + (r.relation() == RelationType.EQ || + r.relation() == RelationType.IN)) { + return true; + } + } + } + return false; + } + /** * Returns the unique resolved value of the specified key from top-level * EQ/IN relations. @@ -377,6 +376,24 @@ public T conditionValue(Object key) { return value; } + /** + * Returns the unique resolved value of the specified key from top-level + * EQ/IN relations, or {@code null} if the resolved candidate set doesn't + * contain exactly one value. + * + * Use this method when callers want "single-or-null" semantics instead of + * treating multiple remaining values as an error. + */ + public T uniqueConditionValue(Object key) { + Set values = this.conditionValues(key); + if (values.size() != 1) { + return null; + } + @SuppressWarnings("unchecked") + T value = (T) values.iterator().next(); + return value; + } + public void unsetCondition(Object key) { this.conditions.removeIf(c -> c.isRelation() && ((Relation) c).key().equals(key)); } @@ -385,6 +402,10 @@ public boolean containsCondition(HugeKeys key) { return this.containsCondition((Object) key); } + public boolean containsConditionValues(HugeKeys key) { + return this.containsConditionValues((Object) key); + } + private void collectConditionValues(Object key, List valuesEQ, List valuesIN) { for (Condition c : this.conditions) { @@ -658,6 +679,15 @@ public boolean hasNeqCondition() { return false; } + public boolean hasUserpropNeqCondition() { + for (Condition.Relation r : this.userpropRelations()) { + if (r.relation() == RelationType.NEQ) { + return true; + } + } + return false; + } + public boolean matchUserpropKeys(List keys) { Set conditionKeys = this.userpropKeys(); return !keys.isEmpty() && conditionKeys.containsAll(keys); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java index 6f1eec58a6..7871c7fdca 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java @@ -674,7 +674,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) { if (direction == null) { direction = Directions.OUT; } - Id label = cq.conditionValue(HugeKeys.LABEL); + Id label = (Id) this.edgeIdConditionValue(cq, HugeKeys.LABEL); BytesBuffer start = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); writePartitionedId(HugeType.EDGE, vertex, start); @@ -722,8 +722,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { int count = 0; BytesBuffer buffer = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); for (HugeKeys key : EdgeId.KEYS) { - Object value = key == HugeKeys.LABEL ? - cq.conditionValue(key) : cq.condition(key); + Object value = this.edgeIdConditionValue(cq, key); if (value != null) { count++; @@ -764,6 +763,17 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { return null; } + private Object edgeIdConditionValue(ConditionQuery cq, HugeKeys key) { + if (key == HugeKeys.LABEL) { + /* + * LABEL may still be represented by multiple top-level EQ/IN + * relations before strict edge-id serialization. + */ + return cq.conditionValue(key); + } + return cq.condition(key); + } + @Override protected Query writeQueryCondition(Query query) { HugeType type = query.resultType(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java index 61830b3c54..cf357d2132 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/TextSerializer.java @@ -457,7 +457,7 @@ private Query writeQueryEdgeRangeCondition(ConditionQuery cq) { if (direction == null) { direction = Directions.OUT; } - Object label = cq.conditionValue(HugeKeys.LABEL); + Object label = this.edgeIdConditionValue(cq, HugeKeys.LABEL); List start = new ArrayList<>(cq.conditionsSize()); start.add(writeEntryId((Id) vertex)); @@ -491,8 +491,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { List condParts = new ArrayList<>(cq.conditionsSize()); for (HugeKeys key : EdgeId.KEYS) { - Object value = key == HugeKeys.LABEL ? - cq.conditionValue(key) : cq.condition(key); + Object value = this.edgeIdConditionValue(cq, key); if (value == null) { break; } @@ -517,6 +516,17 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { return null; } + private Object edgeIdConditionValue(ConditionQuery cq, HugeKeys key) { + if (key == HugeKeys.LABEL) { + /* + * LABEL may still be represented by multiple top-level EQ/IN + * relations before strict edge-id serialization. + */ + return cq.conditionValue(key); + } + return cq.condition(key); + } + @Override protected Query writeQueryCondition(Query query) { ConditionQuery result = (ConditionQuery) query; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java index c282fe384b..850f37ee5f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/store/ram/RamTable.java @@ -269,7 +269,7 @@ public boolean matched(Query query) { int conditionsSize = cq.conditionsSize(); Object owner = cq.condition(HugeKeys.OWNER_VERTEX); Directions direction = cq.condition(HugeKeys.DIRECTION); - Id label = uniqueLabel(cq); + Id label = cq.uniqueConditionValue(HugeKeys.LABEL); if (direction == null && conditionsSize > 1) { for (Condition cond : cq.conditions()) { @@ -316,7 +316,7 @@ private Iterator query(ConditionQuery query) { if (dir == null) { dir = Directions.BOTH; } - Id label = uniqueLabel(query); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (label == null) { label = IdGenerator.ZERO; } @@ -377,14 +377,6 @@ private static void ensureNumberId(Id id) { } } - private static Id uniqueLabel(ConditionQuery query) { - java.util.Set labels = query.conditionValues(HugeKeys.LABEL); - if (labels.size() != 1) { - return null; - } - return (Id) labels.iterator().next(); - } - private static long encode(long target, Directions direction, int label) { // TODO: support property assert (label & 0x0fffffff) == label; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 6b358b2619..c6f105a0d9 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -415,6 +415,8 @@ private IdHolderList queryByLabel(ConditionQuery query) { HugeType queryType = query.resultType(); IndexLabel il = IndexLabel.label(queryType); validateIndexLabel(il); + // Query-by-label builds a label index entry and requires one + // deterministically resolved label instead of best-effort fallback. Id label = query.conditionValue(HugeKeys.LABEL); E.checkState(label != null, "Expect one label value for query: %s", query); @@ -483,7 +485,7 @@ private IdHolderList queryByUserprop(ConditionQuery query) { } Set indexes = this.collectMatchedIndexes(query); if (indexes.isEmpty()) { - Id label = uniqueLabel(query); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); throw noIndexException(this.graph(), query, label); } @@ -757,11 +759,12 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, @Watched(prefix = "index") private Set collectMatchedIndexes(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); - boolean hasLabel = query.containsCondition(HugeKeys.LABEL); + boolean hasLabelValues = query.containsConditionValues(HugeKeys.LABEL); Set labels = query.conditionValues(HugeKeys.LABEL); List schemaLabels; - if (hasLabel && labels.isEmpty()) { + if (hasLabelValues && labels.isEmpty()) { + // LABEL EQ/IN conditions resolve to an empty intersection. return Collections.emptySet(); } if (labels.size() == 1) { @@ -952,7 +955,7 @@ private void removeExpiredIndexIfNeeded(HugeIndex index, private static Set matchSingleOrCompositeIndex( ConditionQuery query, Set indexLabels) { - if (query.hasNeqCondition()) { + if (query.hasUserpropNeqCondition()) { return ImmutableSet.of(); } boolean requireRange = query.hasRangeCondition(); @@ -993,7 +996,7 @@ private static Set matchSingleOrCompositeIndex( private static Set matchJointIndexes( ConditionQuery query, Set indexLabels) { - if (query.hasNeqCondition()) { + if (query.hasUserpropNeqCondition()) { return ImmutableSet.of(); } Set queryPropKeys = query.userpropKeys(); @@ -1788,7 +1791,7 @@ protected long removeIndexLeft(ConditionQuery query, } // Check label is matched - Id label = uniqueLabel(query); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); // NOTE: original condition query may not have label condition, // which means possibly label == null. if (label != null && !element.schemaLabel().id().equals(label)) { @@ -1988,12 +1991,4 @@ public Long reduce(Long t1, Long t2) { return t1 + t2; } } - - private static Id uniqueLabel(ConditionQuery query) { - Set labels = query.conditionValues(HugeKeys.LABEL); - if (labels.size() != 1) { - return null; - } - return (Id) labels.iterator().next(); - } } 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 6c4f174d7d..79e1211908 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 @@ -1059,7 +1059,7 @@ protected Iterator queryEdgesFromBackend(Query query) { ConditionQueryFlatten.flatten((ConditionQuery) query, supportIn).stream(); Stream> edgeIterators = flattenedQueries.map(cq -> { - Id label = uniqueLabel(cq); + Id label = cq.uniqueConditionValue(HugeKeys.LABEL); if (this.storeFeatures().supportsFatherAndSubEdgeLabel() && label != null && graph().edgeLabel(label).isFather() && @@ -1389,7 +1389,7 @@ private static boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = uniqueLabel(query); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (label == null) { return false; } @@ -1522,7 +1522,7 @@ private Query optimizeQuery(ConditionQuery query) { throw new HugeException("Not supported querying by id and conditions: %s", query); } - Id label = uniqueLabel(query); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); // Optimize vertex query if (label != null && query.resultType().isVertex()) { @@ -1914,7 +1914,8 @@ private boolean rightResultFromIndexQuery(Query query, HugeElement elem) { } ConditionQuery cq = (ConditionQuery) query; - if (uniqueLabel(cq) != null && cq.resultType().isEdge()) { + if (cq.uniqueConditionValue(HugeKeys.LABEL) != null && + cq.resultType().isEdge()) { if (cq.conditions().size() == 1) { // g.E().hasLabel(xxx) return true; @@ -1966,14 +1967,6 @@ private boolean rightResultFromIndexQuery(Query query, HugeElement elem) { return false; } - private static Id uniqueLabel(ConditionQuery query) { - Set labels = query.conditionValues(HugeKeys.LABEL); - if (labels.size() != 1) { - return null; - } - return (Id) labels.iterator().next(); - } - private Iterator filterExpiredResultFromBackend( Query query, Iterator results) { if (this.store().features().supportsTtl() || query.showExpired()) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java index a19c32a04f..0785286d3f 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/traversal/algorithm/HugeTraverser.java @@ -582,6 +582,8 @@ private void fillFilterBySortKeys(Query query, Id[] edgeLabels, ConditionQuery condQuery = (ConditionQuery) query; if (!GraphTransaction.matchFullEdgeSortKeys(condQuery, this.graph())) { + // Sort-key validation needs one concrete edge label so that the + // error message points to the exact schema label in use. Id label = condQuery.conditionValue(HugeKeys.LABEL); E.checkArgument(false, "The properties %s does not match " + "sort keys of edge label '%s'", diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java index 713511490d..00bef521d0 100644 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreStore.java @@ -403,7 +403,7 @@ public IdPrefixQuery next() { if (hugeGraph != null) { for (ConditionQuery conditionQuery : ConditionQueryFlatten.flatten(cq)) { - Id label = this.uniqueLabel(conditionQuery); + Id label = conditionQuery.uniqueConditionValue(HugeKeys.LABEL); /* Parent type + sortKeys: g.V("V.id").outE("parentLabel") .has("sortKey","value") converted to all subtypes + sortKeys */ if ((this.subEls == null || @@ -455,19 +455,11 @@ public IdPrefixQuery next() { buffer.bytes(), ownerId)); } - private Id uniqueLabel(ConditionQuery query) { - Set labels = query.conditionValues(HugeKeys.LABEL); - if (labels.size() != 1) { - return null; - } - return (Id) labels.iterator().next(); - } - private boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = this.uniqueLabel(query); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (label == null) { return false; } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index adbdddc24c..6cb743eab6 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -9077,6 +9077,22 @@ public void testQueryByJointLabels() { Assert.assertEquals(0, vertices.size()); } + @Test + public void testQueryByNonEqLabelAndIndexedProperty() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V().has(T.label, P.neq("author")) + .has("city", "Beijing").toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("person", vertex.label()); + } + } + @Test public void testCollectMatchedIndexesByJointLabelsWithIndexedProperties() { HugeGraph graph = graph(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java index bb2951c8ea..b8b505c3d8 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java @@ -53,8 +53,10 @@ public void testConditionWithoutLabel() { ConditionQuery query = new ConditionQuery(HugeType.EDGE); Assert.assertFalse(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); Assert.assertEquals(ImmutableSet.of(), query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.uniqueConditionValue(HugeKeys.LABEL)); Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); Assert.assertNull(query.condition(HugeKeys.LABEL)); } @@ -70,8 +72,10 @@ public void testConditionWithEqAndIn() { ImmutableList.of(label1, label2))); Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); Assert.assertEquals(ImmutableSet.of(label1), query.conditionValues(HugeKeys.LABEL)); + Assert.assertEquals(label1, query.uniqueConditionValue(HugeKeys.LABEL)); Assert.assertEquals(label1, query.conditionValue(HugeKeys.LABEL)); Assert.assertEquals(label1, query.condition(HugeKeys.LABEL)); } @@ -86,8 +90,10 @@ public void testConditionWithSingleInValues() { ImmutableList.of(label1, label2))); Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); Assert.assertEquals(ImmutableSet.of(label1, label2), query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.uniqueConditionValue(HugeKeys.LABEL)); Assert.assertThrows(IllegalStateException.class, () -> query.conditionValue(HugeKeys.LABEL), e -> Assert.assertContains("Illegal key 'LABEL'", @@ -109,12 +115,43 @@ public void testConditionWithConflictingEqAndIn() { ImmutableList.of(label1, label3))); Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); Assert.assertEquals(ImmutableSet.of(), query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.uniqueConditionValue(HugeKeys.LABEL)); Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); Assert.assertNull(query.condition(HugeKeys.LABEL)); } + @Test + public void testConditionWithNonEqInLabel() { + Id label = IdGenerator.of(1); + + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.neq(HugeKeys.LABEL, label); + + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertTrue(query.hasNeqCondition()); + Assert.assertFalse(query.hasUserpropNeqCondition()); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.uniqueConditionValue(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + Assert.assertNull(query.condition(HugeKeys.LABEL)); + } + + @Test + public void testConditionWithUserpropNeq() { + Id prop = IdGenerator.of(1); + + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.neq(prop, "Beijing")); + + Assert.assertTrue(query.hasNeqCondition()); + Assert.assertTrue(query.hasUserpropNeqCondition()); + } + @Test public void testConditionWithMultipleMatchedInValues() { Id label1 = IdGenerator.of(1); @@ -129,8 +166,10 @@ public void testConditionWithMultipleMatchedInValues() { ImmutableList.of(label1, label2, label4))); Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); Assert.assertEquals(ImmutableSet.of(label1, label2), query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.uniqueConditionValue(HugeKeys.LABEL)); Assert.assertThrows(IllegalStateException.class, () -> query.conditionValue(HugeKeys.LABEL), e -> Assert.assertContains("Illegal key 'LABEL'", From a326f44c5b7797aed2cd647895e366d5ac3c28c8 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Thu, 4 Jun 2026 20:54:48 +0800 Subject: [PATCH 04/11] test(core): cover edge label query semantics --- .../apache/hugegraph/core/EdgeCoreTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java index bbf7db6562..fc7ed5edc6 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java @@ -3582,6 +3582,43 @@ public void testQueryOutEdgesOfVertexBySortkeyAndProps() { Assert.assertEquals(0, edges.size()); } + @Test + public void testQueryOutEdgesBySingleResolvedLabelAndSortKey() { + HugeGraph graph = graph(); + Vertex reader = initEdgeLabelQueryEdges(); + + List edges = graph.traversal().V(reader.id()) + .outE("reviewed") + .has(T.label, P.within("reviewed", + "recommended")) + .has("time", "2026-1-1") + .toList(); + + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("reviewed", edges.get(0).label()); + Assert.assertEquals("2026-1-1", edges.get(0).value("time")); + } + + @Test + public void testQueryOutEdgesByMultiLabelsAndSortKey() { + HugeGraph graph = graph(); + Vertex reader = initEdgeLabelQueryEdges(); + + List edges = graph.traversal().V(reader.id()) + .outE("reviewed", "recommended") + .has("time", "2026-1-1") + .toList(); + + Set labels = new HashSet<>(); + for (Edge edge : edges) { + labels.add(edge.label()); + Assert.assertEquals("2026-1-1", edge.value("time")); + } + Assert.assertEquals(2, edges.size()); + Assert.assertEquals(ImmutableSet.of("reviewed", "recommended"), + labels); + } + @Test public void testQueryOutEdgesOfVertexBySortkeyWithRange() { // FIXME: skip this test for hstore @@ -7691,6 +7728,40 @@ private void init18Edges(boolean commit) { } } + private Vertex initEdgeLabelQueryEdges() { + HugeGraph graph = graph(); + SchemaManager schema = graph.schema(); + + schema.edgeLabel("reviewed").properties("time", "score") + .multiTimes().sortKeys("time") + .link("person", "book") + .enableLabelIndex(false) + .create(); + schema.edgeLabel("recommended").properties("time", "score") + .multiTimes().sortKeys("time") + .link("person", "book") + .enableLabelIndex(false) + .create(); + + Vertex reader = graph.addVertex(T.label, "person", + "name", "edge-label-reader", + "city", "Beijing", + "age", 29); + Vertex book1 = graph.addVertex(T.label, "book", + "name", "edge-label-book-1"); + Vertex book2 = graph.addVertex(T.label, "book", + "name", "edge-label-book-2"); + Vertex book3 = graph.addVertex(T.label, "book", + "name", "edge-label-book-3"); + + reader.addEdge("reviewed", book1, "time", "2026-1-1", "score", 1); + reader.addEdge("recommended", book2, "time", "2026-1-1", "score", 2); + reader.addEdge("reviewed", book3, "time", "2026-1-2", "score", 3); + + graph.tx().commit(); + return reader; + } + private void init100LookEdges() { HugeGraph graph = graph(); From b10e3c24fe2f24079bddf06ae38385b38b3fbdaf Mon Sep 17 00:00:00 2001 From: contrueCT Date: Fri, 5 Jun 2026 13:08:44 +0800 Subject: [PATCH 05/11] fix(core): tolerate missing related index labels --- .../hugegraph/backend/tx/GraphIndexTransaction.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index c6f105a0d9..f24de2faf2 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -1564,8 +1564,13 @@ private static Set relatedIndexLabels(HugeElement element) { Set indexLabelIds = element.schemaLabel().indexLabels(); for (Id id : indexLabelIds) { - IndexLabel indexLabel = element.graph().indexLabel(id); - indexLabels.add(indexLabel); + try { + IndexLabel indexLabel = element.graph().indexLabel(id); + indexLabels.add(indexLabel); + } catch (IllegalArgumentException e) { + LOG.debug("Skip missing related index label '{}' of element {}", + id, element.id(), e); + } } return indexLabels; } From a1835719c3382f58c2e85aa46daefa34c9967b2a Mon Sep 17 00:00:00 2001 From: contrueCT Date: Fri, 5 Jun 2026 13:40:13 +0800 Subject: [PATCH 06/11] fix(core): skip stale index entries --- .../backend/tx/GraphIndexTransaction.java | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index f24de2faf2..09e2653237 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -685,8 +685,11 @@ private IdHolder doIndexQueryBatch(IndexLabel indexLabel, Set ids = InsertionOrderUtil.newSet(); while ((batch == Query.NO_LIMIT || ids.size() < batch) && entries.hasNext()) { - HugeIndex index = this.serializer.readIndex(graph(), query, - entries.next()); + HugeIndex index = this.readMatchedIndex(indexLabel, query, + entries.next()); + if (index == null) { + continue; + } this.removeExpiredIndexIfNeeded(index, query.showExpired()); ids.addAll(index.elementIds()); Query.checkForceCapacity(ids.size()); @@ -727,8 +730,11 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, Set ids = InsertionOrderUtil.newSet(); entries = super.query(query).iterator(); while (entries.hasNext()) { - HugeIndex index = this.serializer.readIndex(graph(), query, - entries.next()); + HugeIndex index = this.readMatchedIndex(indexLabel, query, + entries.next()); + if (index == null) { + continue; + } this.removeExpiredIndexIfNeeded(index, query.showExpired()); ids.addAll(index.elementIds()); if (query.reachLimit(ids.size())) { @@ -756,6 +762,34 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, } } + private HugeIndex readMatchedIndex(IndexLabel indexLabel, + ConditionQuery query, + BackendEntry entry) { + HugeIndex index; + try { + index = this.serializer.readIndex(graph(), query, entry); + } catch (IllegalArgumentException e) { + if (!missingIndexLabel(e)) { + throw e; + } + LOG.debug("Skip stale index entry with missing index label while " + + "querying index label '{}'", indexLabel.id(), e); + return null; + } + if (!Objects.equals(index.indexLabelId(), indexLabel.id())) { + LOG.debug("Skip stale index entry of index label '{}' while " + + "querying index label '{}'", + index.indexLabelId(), indexLabel.id()); + return null; + } + return index; + } + + private static boolean missingIndexLabel(IllegalArgumentException e) { + String message = e.getMessage(); + return message != null && message.contains("Undefined index label"); + } + @Watched(prefix = "index") private Set collectMatchedIndexes(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); From ebc31c831ac8d152208f131830ffd76920db9d4b Mon Sep 17 00:00:00 2001 From: contrueCT Date: Sat, 6 Jun 2026 02:04:49 +0800 Subject: [PATCH 07/11] fix(core): stabilize hstore range index ordering --- .../backend/tx/GraphIndexTransaction.java | 185 +++++++++++++++++- 1 file changed, 183 insertions(+), 2 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 09e2653237..bb0f90f5a0 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -651,6 +651,9 @@ private void storeSelectedIndexField(IndexLabel indexLabel, @Watched(prefix = "index") private IdHolder doIndexQuery(IndexLabel indexLabel, ConditionQuery query) { + if (this.needHstoreRangeIndexOrder(indexLabel)) { + return this.doHstoreRangeIndexQuery(indexLabel, query); + } if (!query.paging()) { return this.doIndexQueryBatch(indexLabel, query); } else { @@ -660,6 +663,184 @@ private IdHolder doIndexQuery(IndexLabel indexLabel, ConditionQuery query) { } } + private boolean needHstoreRangeIndexOrder(IndexLabel indexLabel) { + return this.store().provider().isHstore() && + indexLabel.indexType().isRange(); + } + + private IdHolder doHstoreRangeIndexQuery(IndexLabel indexLabel, + ConditionQuery query) { + if (!query.paging()) { + if (query.noLimitAndOffset()) { + return this.doIndexQueryBatch(indexLabel, query); + } + Set ids = this.querySortedRangeIndexIds(indexLabel, query); + return this.newSortedRangeIndexBatchHolder(query, ids); + } + return new PagingIdHolder(query, q -> { + return this.querySortedRangeIndexPage(indexLabel, q); + }); + } + + private BatchIdHolder newSortedRangeIndexBatchHolder(ConditionQuery query, + Set ids) { + List idList = new ArrayList<>(ids); + return new BatchIdHolder(query, Collections.emptyIterator(), batch -> { + throw new IllegalStateException("Unexpected sorted index fetcher"); + }) { + private int offset = 0; + + @Override + public boolean hasNext() { + return this.offset < idList.size(); + } + + @Override + public IdHolder next() { + if (!this.hasNext()) { + throw new java.util.NoSuchElementException(); + } + return this; + } + + @Override + public PageIds fetchNext(String page, long batchSize) { + E.checkArgument(page == null, + "Not support page parameter by BatchIdHolder"); + if (!this.hasNext()) { + return PageIds.EMPTY; + } + + int end; + if (batchSize == Query.NO_LIMIT) { + end = idList.size(); + } else { + end = (int) Math.min((long) idList.size(), + this.offset + batchSize); + } + Set batchIds = InsertionOrderUtil.newSet(); + batchIds.addAll(idList.subList(this.offset, end)); + this.offset = end; + return new PageIds(batchIds, PageState.EMPTY); + } + + @Override + public Set all() { + Set allIds = InsertionOrderUtil.newSet(); + allIds.addAll(idList); + return allIds; + } + + @Override + public void close() { + this.offset = idList.size(); + } + }; + } + + private Set querySortedRangeIndexIds(IndexLabel indexLabel, + ConditionQuery query) { + List indexes = this.querySortedRangeIndexes(indexLabel, + query); + Set ids = InsertionOrderUtil.newSet(); + for (HugeIndex index : indexes) { + ids.addAll(index.elementIds()); + Query.checkForceCapacity(ids.size()); + } + return ids; + } + + private PageIds querySortedRangeIndexPage(IndexLabel indexLabel, + ConditionQuery query) { + List indexes = this.querySortedRangeIndexes(indexLabel, + query); + Set allIds = InsertionOrderUtil.newSet(); + for (HugeIndex index : indexes) { + allIds.addAll(index.elementIds()); + Query.checkForceCapacity(allIds.size()); + } + if (allIds.isEmpty()) { + return PageIds.EMPTY; + } + + int start = 0; + if (!query.page().isEmpty()) { + start = PageState.fromString(query.page()).offset(); + } + if (start >= allIds.size()) { + return PageIds.EMPTY; + } + + long total = allIds.size(); + long end = query.noLimit() ? total : + Math.min(total, (long) start + query.limit()); + Set pageIds = CollectionUtil.subSet(allIds, start, (int) end); + if (pageIds.isEmpty()) { + return PageIds.EMPTY; + } + + int next = (int) end; + PageState pageState; + if (next < total) { + pageState = new PageState(new byte[]{1}, next, pageIds.size()); + } else { + pageState = new PageState(PageState.EMPTY_BYTES, 0, + pageIds.size()); + } + return new PageIds(pageIds, pageState); + } + + private List querySortedRangeIndexes(IndexLabel indexLabel, + ConditionQuery query) { + List indexes = new ArrayList<>(); + Iterator entries = null; + String spaceGraph = this.params() + .graph().spaceGraphName(); + LockUtil.Locks locks = new LockUtil.Locks(spaceGraph); + ConditionQuery scanQuery = query.copy(); + scanQuery.page(null); + scanQuery.limit(Query.NO_LIMIT); + try { + locks.lockReads(LockUtil.INDEX_LABEL_DELETE, indexLabel.id()); + locks.lockReads(LockUtil.INDEX_LABEL_REBUILD, indexLabel.id()); + if (!indexLabel.system()) { + graph().indexLabel(indexLabel.id()); + } + + entries = super.query(scanQuery).iterator(); + while (entries.hasNext()) { + HugeIndex index = this.readMatchedIndex(indexLabel, scanQuery, + entries.next()); + if (index == null) { + continue; + } + this.removeExpiredIndexIfNeeded(index, scanQuery.showExpired()); + this.recordIndexValue(scanQuery, index); + indexes.add(index); + Query.checkForceCapacity(indexes.size()); + } + } finally { + locks.unlock(); + CloseableIterator.closeIterator(entries); + } + + Collections.sort(indexes, (a, b) -> { + return this.compareRangeIndexValues(a, b); + }); + return indexes; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private int compareRangeIndexValues(HugeIndex left, HugeIndex right) { + Object leftValue = left.fieldValues(); + Object rightValue = right.fieldValues(); + E.checkArgument(leftValue instanceof Comparable, + "Invalid range index value '%s'", leftValue); + E.checkArgument(rightValue instanceof Comparable, + "Invalid range index value '%s'", rightValue); + return ((Comparable) leftValue).compareTo(rightValue); + } + @Watched(prefix = "index") private IdHolder doIndexQueryBatch(IndexLabel indexLabel, ConditionQuery query) { @@ -772,8 +953,8 @@ private HugeIndex readMatchedIndex(IndexLabel indexLabel, if (!missingIndexLabel(e)) { throw e; } - LOG.debug("Skip stale index entry with missing index label while " + - "querying index label '{}'", indexLabel.id(), e); + LOG.debug("Skip stale index entry with missing index label " + + "while querying index label '{}'", indexLabel.id(), e); return null; } if (!Objects.equals(index.indexLabelId(), indexLabel.id())) { From 2df480248f24f5ecd601d2222c56c05162f9f1f6 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Sun, 7 Jun 2026 11:05:00 +0800 Subject: [PATCH 08/11] fix(core): reset hstore range scan offset --- .../hugegraph/backend/tx/GraphIndexTransaction.java | 1 + .../java/org/apache/hugegraph/core/VertexCoreTest.java | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index bb0f90f5a0..ca51ec9b6e 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -799,6 +799,7 @@ private List querySortedRangeIndexes(IndexLabel indexLabel, LockUtil.Locks locks = new LockUtil.Locks(spaceGraph); ConditionQuery scanQuery = query.copy(); scanQuery.page(null); + scanQuery.offset(0L); scanQuery.limit(Query.NO_LIMIT); try { locks.lockReads(LockUtil.INDEX_LABEL_DELETE, indexLabel.id()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index 6cb743eab6..bf2714aea1 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -4415,6 +4415,14 @@ public void testQueryByDateProperty() { Assert.assertEquals(dates[1], vertices.get(0).value("birth")); Assert.assertEquals(dates[2], vertices.get(1).value("birth")); + // range with offset + vertices = graph.traversal().V().hasLabel("person") + .has("birth", P.between(dates[1], dates[4])) + .range(1, 3).toList(); + Assert.assertEquals(2, vertices.size()); + Assert.assertEquals(dates[2], vertices.get(0).value("birth")); + Assert.assertEquals(dates[3], vertices.get(1).value("birth")); + // limit after delete graph.traversal().V().hasLabel("person") .has("birth", P.between(dates[1], dates[4])) From 939cc12d74e780275644d51e99d439f3334d785c Mon Sep 17 00:00:00 2001 From: contrueCT Date: Mon, 8 Jun 2026 16:49:35 +0800 Subject: [PATCH 09/11] fix(core): preserve sorted hstore range ordering --- .../hugegraph/backend/page/QueryList.java | 5 +- .../hugegraph/backend/query/QueryResults.java | 8 +- .../backend/tx/GraphIndexTransaction.java | 171 ++++++++++++------ .../backend/tx/GraphTransaction.java | 11 +- .../backend/tx/GraphIndexTransactionTest.java | 82 +++++++++ .../apache/hugegraph/unit/UnitTestSuite.java | 4 + .../hugegraph/unit/core/QueryResultsTest.java | 82 +++++++++ 7 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java index d1e11e9220..f1930bd55a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/QueryList.java @@ -258,7 +258,7 @@ private QueryResults each(IdHolder holder) { return null; } - return this.queryByIndexIds(ids); + return this.queryByIndexIds(ids, holder.keepOrder()); }); } @@ -275,7 +275,8 @@ public PageResults iterator(int index, String page, long pageSize) { return PageResults.emptyIterator(); } - QueryResults results = this.queryByIndexIds(pageIds.ids()); + QueryResults results = this.queryByIndexIds(pageIds.ids(), + holder.keepOrder()); return new PageResults<>(results, pageIds.pageState()); } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java index a03e5c9aee..48e06b2afe 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/QueryResults.java @@ -102,12 +102,12 @@ public Iterator keepInputOrderIfNeeded( return origin; } Collection ids; - if (!this.mustSortByInputIds() || this.paging() || + if (!this.mustSortByInputIds() || (ids = this.queryIds()).size() <= 1) { /* - * Return the original iterator if it's paging query or if the - * query input is less than one id, or don't have to do sort. - * NOTE: queryIds() only return the first batch of index query + * Return the original iterator if the query input is less than one + * id, or don't have to do sort. + * NOTE: queryIds() only return the first batch of index query. */ return origin; } diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index ca51ec9b6e..d9ddbd322b 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -677,65 +677,14 @@ private IdHolder doHstoreRangeIndexQuery(IndexLabel indexLabel, Set ids = this.querySortedRangeIndexIds(indexLabel, query); return this.newSortedRangeIndexBatchHolder(query, ids); } - return new PagingIdHolder(query, q -> { + return new SortedRangePagingIdHolder(query, q -> { return this.querySortedRangeIndexPage(indexLabel, q); }); } private BatchIdHolder newSortedRangeIndexBatchHolder(ConditionQuery query, Set ids) { - List idList = new ArrayList<>(ids); - return new BatchIdHolder(query, Collections.emptyIterator(), batch -> { - throw new IllegalStateException("Unexpected sorted index fetcher"); - }) { - private int offset = 0; - - @Override - public boolean hasNext() { - return this.offset < idList.size(); - } - - @Override - public IdHolder next() { - if (!this.hasNext()) { - throw new java.util.NoSuchElementException(); - } - return this; - } - - @Override - public PageIds fetchNext(String page, long batchSize) { - E.checkArgument(page == null, - "Not support page parameter by BatchIdHolder"); - if (!this.hasNext()) { - return PageIds.EMPTY; - } - - int end; - if (batchSize == Query.NO_LIMIT) { - end = idList.size(); - } else { - end = (int) Math.min((long) idList.size(), - this.offset + batchSize); - } - Set batchIds = InsertionOrderUtil.newSet(); - batchIds.addAll(idList.subList(this.offset, end)); - this.offset = end; - return new PageIds(batchIds, PageState.EMPTY); - } - - @Override - public Set all() { - Set allIds = InsertionOrderUtil.newSet(); - allIds.addAll(idList); - return allIds; - } - - @Override - public void close() { - this.offset = idList.size(); - } - }; + return new SortedRangeBatchIdHolder(query, ids); } private Set querySortedRangeIndexIds(IndexLabel indexLabel, @@ -842,6 +791,122 @@ private int compareRangeIndexValues(HugeIndex left, HugeIndex right) { return ((Comparable) leftValue).compareTo(rightValue); } + static class SortedRangeBatchIdHolder extends BatchIdHolder { + + private final List idList; + private int offset; + private PageIds pendingBatch; + + SortedRangeBatchIdHolder(ConditionQuery query, Set ids) { + super(query, Collections.emptyIterator(), batch -> { + throw new IllegalStateException("Unexpected sorted index fetcher"); + }); + this.idList = new ArrayList<>(ids); + this.offset = 0; + this.pendingBatch = null; + } + + @Override + public boolean keepOrder() { + return true; + } + + @Override + public boolean hasNext() { + if (this.pendingBatch != null) { + return true; + } + if (this.exhausted) { + return false; + } + return this.offset < this.idList.size(); + } + + @Override + public IdHolder next() { + if (!this.hasNext()) { + throw new java.util.NoSuchElementException(); + } + return this; + } + + @Override + public PageIds fetchNext(String page, long batchSize) { + E.checkArgument(page == null, + "Not support page parameter by BatchIdHolder"); + E.checkArgument(batchSize >= 0L, + "Invalid batch size value: %s", batchSize); + if (this.pendingBatch != null) { + PageIds result = this.pendingBatch; + this.pendingBatch = null; + return result; + } + return this.fetchBatch(batchSize); + } + + @Override + public Set all() { + Set allIds = InsertionOrderUtil.newSet(); + if (this.pendingBatch != null) { + allIds.addAll(this.pendingBatch.ids()); + } + if (this.offset < this.idList.size()) { + allIds.addAll(this.idList.subList(this.offset, + this.idList.size())); + } + this.close(); + return allIds; + } + + @Override + public PageIds peekNext(long size) { + E.checkArgument(this.pendingBatch == null, + "Can't call peekNext() twice"); + this.pendingBatch = this.fetchBatch(size); + return this.pendingBatch; + } + + @Override + public void close() { + this.exhausted = true; + this.pendingBatch = null; + this.offset = this.idList.size(); + } + + private PageIds fetchBatch(long batchSize) { + if (this.offset >= this.idList.size() || batchSize == 0L) { + this.close(); + return PageIds.EMPTY; + } + + int end; + if (batchSize == Query.NO_LIMIT) { + end = this.idList.size(); + } else { + end = (int) Math.min((long) this.idList.size(), + this.offset + batchSize); + } + Set batchIds = InsertionOrderUtil.newSet(); + batchIds.addAll(this.idList.subList(this.offset, end)); + this.offset = end; + this.exhausted = this.offset >= this.idList.size(); + return new PageIds(batchIds, PageState.EMPTY); + } + } + + private static class SortedRangePagingIdHolder extends PagingIdHolder { + + SortedRangePagingIdHolder(ConditionQuery query, + Function fetcher) { + super(query, fetcher); + } + + @Override + public boolean keepOrder() { + return true; + } + } + @Watched(prefix = "index") private IdHolder doIndexQueryBatch(IndexLabel indexLabel, ConditionQuery query) { 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 79e1211908..0a4653c019 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 @@ -865,7 +865,8 @@ protected Iterator queryVerticesFromBackend(Query query) { this::parseEntry); vertices = this.filterExpiredResultFromBackend(query, vertices); - if (!this.store().features().supportsQuerySortByInputIds()) { + if (!this.store().features().supportsQuerySortByInputIds() || + this.needKeepInputOrder(query)) { // There is no id in BackendEntry, so sort after deserialization vertices = results.keepInputOrderIfNeeded(vertices); } @@ -1104,13 +1105,19 @@ private Iterator queryEdgesFromBackendInternal(Query query) { edges = this.filterExpiredResultFromBackend(query, edges); - if (!this.store().features().supportsQuerySortByInputIds()) { + if (!this.store().features().supportsQuerySortByInputIds() || + this.needKeepInputOrder(query)) { // There is no id in BackendEntry, so sort after deserialization edges = results.keepInputOrderIfNeeded(edges); } return edges; } + private boolean needKeepInputOrder(Query query) { + return query instanceof IdQuery && + ((IdQuery) query).mustSortByInput(); + } + private Iterator parentElQueryWithSortKeys(EdgeLabel label, Collection allEls, ConditionQuery cq) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java new file mode 100644 index 0000000000..a1b0d4e8c6 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java @@ -0,0 +1,82 @@ +/* + * 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.backend.tx; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.page.PageIds; +import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.util.InsertionOrderUtil; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +public class GraphIndexTransactionTest { + + @Test + public void testSortedRangeBatchHolderKeepsPeekedBatch() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + Id id1 = IdGenerator.of(1); + Id id2 = IdGenerator.of(2); + Id id3 = IdGenerator.of(3); + Set ids = InsertionOrderUtil.newSet(); + ids.add(id1); + ids.add(id2); + ids.add(id3); + + GraphIndexTransaction.SortedRangeBatchIdHolder holder = + new GraphIndexTransaction.SortedRangeBatchIdHolder(query, ids); + + Assert.assertTrue(holder.keepOrder()); + + PageIds peeked = holder.peekNext(2); + Assert.assertEquals(ImmutableList.of(id1, id2), asList(peeked.ids())); + + PageIds firstBatch = holder.fetchNext(null, 2); + Assert.assertEquals(ImmutableList.of(id1, id2), + asList(firstBatch.ids())); + + PageIds secondBatch = holder.fetchNext(null, 2); + Assert.assertEquals(ImmutableList.of(id3), asList(secondBatch.ids())); + Assert.assertFalse(holder.hasNext()); + } + + @Test + public void testSortedRangeBatchHolderClosesOnZeroBatch() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + Set ids = InsertionOrderUtil.newSet(); + ids.add(IdGenerator.of(1)); + + GraphIndexTransaction.SortedRangeBatchIdHolder holder = + new GraphIndexTransaction.SortedRangeBatchIdHolder(query, ids); + + PageIds batch = holder.fetchNext(null, 0); + Assert.assertTrue(batch.empty()); + Assert.assertFalse(holder.hasNext()); + } + + private static List asList(Set ids) { + return new ArrayList<>(ids); + } +} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index fb7f0e744b..9935909a02 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -17,6 +17,7 @@ package org.apache.hugegraph.unit; +import org.apache.hugegraph.backend.tx.GraphIndexTransactionTest; import org.apache.hugegraph.core.RoleElectionStateMachineTest; import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest; @@ -40,6 +41,7 @@ import org.apache.hugegraph.unit.core.ExceptionTest; import org.apache.hugegraph.unit.core.LocksTableTest; import org.apache.hugegraph.unit.core.PageStateTest; +import org.apache.hugegraph.unit.core.QueryResultsTest; import org.apache.hugegraph.unit.core.QueryTest; import org.apache.hugegraph.unit.core.RangeTest; import org.apache.hugegraph.unit.core.RolePermissionTest; @@ -121,7 +123,9 @@ BackendMutationTest.class, ConditionTest.class, ConditionQueryFlattenTest.class, + GraphIndexTransactionTest.class, QueryTest.class, + QueryResultsTest.class, RangeTest.class, SecurityManagerTest.class, RolePermissionTest.class, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java new file mode 100644 index 0000000000..3f1df40728 --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryResultsTest.java @@ -0,0 +1,82 @@ +/* + * 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.unit.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.apache.hugegraph.backend.id.Id; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.query.IdQuery; +import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.backend.query.QueryResults; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.Idfiable; +import org.apache.hugegraph.util.InsertionOrderUtil; +import org.junit.Test; + +import com.google.common.collect.ImmutableList; + +public class QueryResultsTest { + + @Test + public void testKeepInputOrderForPagingIdQuery() { + Id id1 = IdGenerator.of(1); + Id id2 = IdGenerator.of(2); + Query pagingQuery = new Query(HugeType.VERTEX); + pagingQuery.page("page-1"); + pagingQuery.limit(2L); + + Set ids = InsertionOrderUtil.newSet(); + ids.add(id2); + ids.add(id1); + + IdQuery idQuery = new IdQuery(pagingQuery, ids); + idQuery.mustSortByInput(true); + + QueryResults results = new QueryResults<>( + Arrays.asList(new TestIdfiable(id1), + new TestIdfiable(id2)).iterator(), + idQuery); + + List orderedIds = new ArrayList<>(); + results.keepInputOrderIfNeeded(Arrays.asList(new TestIdfiable(id1), + new TestIdfiable(id2)) + .iterator()) + .forEachRemaining(item -> orderedIds.add(item.id())); + + Assert.assertEquals(ImmutableList.of(id2, id1), orderedIds); + } + + private static final class TestIdfiable implements Idfiable { + + private final Id id; + + private TestIdfiable(Id id) { + this.id = id; + } + + @Override + public Id id() { + return this.id; + } + } +} From 9300691ada8fada31296ca7124448d7716850f99 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Thu, 11 Jun 2026 19:33:45 +0800 Subject: [PATCH 10/11] fix(core): handle non-eq label-only traversals --- .../backend/tx/GraphIndexTransaction.java | 5 +++-- .../traversal/optimize/TraversalUtil.java | 22 +++++++++++++++++++ .../apache/hugegraph/core/EdgeCoreTest.java | 14 ++++++++++++ .../apache/hugegraph/core/VertexCoreTest.java | 14 ++++++++++++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index d9ddbd322b..389904392a 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -400,9 +400,10 @@ public IdHolderList queryIndex(ConditionQuery query) { // Query by index query.optimized(OptimizedType.INDEX); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (query.allSysprop() && conds.size() == 1 && - query.containsCondition(HugeKeys.LABEL)) { - // Query only by label + label != null) { + // Query only by one EQ/IN-resolved label return this.queryByLabel(query); } else { // Query by userprops (or userprops + label) 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 7b68f71778..9648a0f8a3 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 @@ -445,6 +445,12 @@ private static boolean extractHasContainers(HugeVertexStep newStep, private static boolean canExtractHasContainers(HugeGraph graph, HasContainerHolder holder) { + List hasContainers = holder.getHasContainers(); + // Keep pure label non-EQ/IN predicates on GraphStep for TinkerPop filtering. + if (hasContainers.size() == 1 && + isOnlyNonEqInLabelPredicate(hasContainers.get(0))) { + return false; + } for (HasContainer has : holder.getHasContainers()) { if (!canExtractHasContainer(graph, has)) { return false; @@ -453,6 +459,22 @@ private static boolean canExtractHasContainers(HugeGraph graph, return true; } + private static boolean isOnlyNonEqInLabelPredicate(HasContainer has) { + if (!has.getKey().equals(T.label.getAccessor())) { + return false; + } + + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P predicate : predicates) { + BiPredicate bp = predicate.getBiPredicate(); + if (bp == Compare.eq || bp == Contains.within) { + return false; + } + } + return true; + } + static boolean canExtractHasContainer(HugeGraph graph, HasContainer has) { if (isSysProp(has.getKey())) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java index fc7ed5edc6..a683486e4c 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java @@ -3619,6 +3619,20 @@ public void testQueryOutEdgesByMultiLabelsAndSortKey() { labels); } + @Test + public void testQueryEdgesByNonEqLabel() { + HugeGraph graph = graph(); + init18Edges(); + + List edges = graph.traversal().E() + .has(T.label, P.neq("created")) + .toList(); + Assert.assertEquals(16, edges.size()); + for (Edge edge : edges) { + Assert.assertNotEquals("created", edge.label()); + } + } + @Test public void testQueryOutEdgesOfVertexBySortkeyWithRange() { // FIXME: skip this test for hstore diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index bf2714aea1..d8f0b4c35b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -9101,6 +9101,20 @@ public void testQueryByNonEqLabelAndIndexedProperty() { } } + @Test + public void testQueryByNonEqLabel() { + HugeGraph graph = graph(); + init10Vertices(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V().has(T.label, P.neq("author")).toList(); + Assert.assertEquals(8, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertNotEquals("author", vertex.label()); + } + } + @Test public void testCollectMatchedIndexesByJointLabelsWithIndexedProperties() { HugeGraph graph = graph(); From 7c85ccad14009eef37573d1c0508a962022e102d Mon Sep 17 00:00:00 2001 From: contrueCT Date: Sat, 13 Jun 2026 23:53:25 +0800 Subject: [PATCH 11/11] fix(hstore): preserve range index paging order --- .../hugegraph/backend/page/IdHolder.java | 25 +- .../backend/tx/GraphIndexTransaction.java | 248 +----------------- .../backend/store/hstore/HstoreSessions.java | 25 ++ .../store/hstore/HstoreSessionsImpl.java | 36 ++- .../backend/store/hstore/HstoreTable.java | 19 +- .../backend/tx/GraphIndexTransactionTest.java | 82 ------ .../apache/hugegraph/core/VertexCoreTest.java | 30 +++ .../apache/hugegraph/unit/UnitTestSuite.java | 2 - .../store/client/NodeTxSessionProxy.java | 15 +- .../store/client/OrderedKvIterator.java | 150 +++++++++++ .../store/client/OrderedKvIteratorTest.java | 173 ++++++++++++ 11 files changed, 461 insertions(+), 344 deletions(-) delete mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java create mode 100644 hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/OrderedKvIterator.java create mode 100644 hugegraph-store/hg-store-client/src/test/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java index b420648767..ab64959a01 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java @@ -35,11 +35,17 @@ public abstract class IdHolder { protected final Query query; + private final boolean keepOrder; protected boolean exhausted; public IdHolder(Query query) { + this(query, false); + } + + public IdHolder(Query query, boolean keepOrder) { E.checkNotNull(query, "query"); this.query = query; + this.keepOrder = keepOrder; this.exhausted = false; } @@ -48,7 +54,7 @@ public Query query() { } public boolean keepOrder() { - return false; + return this.keepOrder; } @Override @@ -97,7 +103,13 @@ public static class PagingIdHolder extends IdHolder { public PagingIdHolder(ConditionQuery query, Function fetcher) { - super(query.copy()); + this(query, fetcher, false); + } + + public PagingIdHolder(ConditionQuery query, + Function fetcher, + boolean keepOrder) { + super(query.copy(), keepOrder); E.checkArgument(query.paging(), "Query '%s' must include page info", query); this.fetcher = fetcher; @@ -142,7 +154,14 @@ public static class BatchIdHolder extends IdHolder public BatchIdHolder(ConditionQuery query, Iterator entries, Function> fetcher) { - super(query); + this(query, entries, fetcher, false); + } + + public BatchIdHolder(ConditionQuery query, + Iterator entries, + Function> fetcher, + boolean keepOrder) { + super(query, keepOrder); this.entries = entries; this.fetcher = fetcher; this.count = 0L; diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 389904392a..7eeed8d081 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java @@ -652,262 +652,20 @@ private void storeSelectedIndexField(IndexLabel indexLabel, @Watched(prefix = "index") private IdHolder doIndexQuery(IndexLabel indexLabel, ConditionQuery query) { - if (this.needHstoreRangeIndexOrder(indexLabel)) { - return this.doHstoreRangeIndexQuery(indexLabel, query); - } if (!query.paging()) { return this.doIndexQueryBatch(indexLabel, query); } else { return new PagingIdHolder(query, q -> { return this.doIndexQueryOnce(indexLabel, q); - }); + }, this.keepBackendIndexOrder(indexLabel)); } } - private boolean needHstoreRangeIndexOrder(IndexLabel indexLabel) { + private boolean keepBackendIndexOrder(IndexLabel indexLabel) { return this.store().provider().isHstore() && indexLabel.indexType().isRange(); } - private IdHolder doHstoreRangeIndexQuery(IndexLabel indexLabel, - ConditionQuery query) { - if (!query.paging()) { - if (query.noLimitAndOffset()) { - return this.doIndexQueryBatch(indexLabel, query); - } - Set ids = this.querySortedRangeIndexIds(indexLabel, query); - return this.newSortedRangeIndexBatchHolder(query, ids); - } - return new SortedRangePagingIdHolder(query, q -> { - return this.querySortedRangeIndexPage(indexLabel, q); - }); - } - - private BatchIdHolder newSortedRangeIndexBatchHolder(ConditionQuery query, - Set ids) { - return new SortedRangeBatchIdHolder(query, ids); - } - - private Set querySortedRangeIndexIds(IndexLabel indexLabel, - ConditionQuery query) { - List indexes = this.querySortedRangeIndexes(indexLabel, - query); - Set ids = InsertionOrderUtil.newSet(); - for (HugeIndex index : indexes) { - ids.addAll(index.elementIds()); - Query.checkForceCapacity(ids.size()); - } - return ids; - } - - private PageIds querySortedRangeIndexPage(IndexLabel indexLabel, - ConditionQuery query) { - List indexes = this.querySortedRangeIndexes(indexLabel, - query); - Set allIds = InsertionOrderUtil.newSet(); - for (HugeIndex index : indexes) { - allIds.addAll(index.elementIds()); - Query.checkForceCapacity(allIds.size()); - } - if (allIds.isEmpty()) { - return PageIds.EMPTY; - } - - int start = 0; - if (!query.page().isEmpty()) { - start = PageState.fromString(query.page()).offset(); - } - if (start >= allIds.size()) { - return PageIds.EMPTY; - } - - long total = allIds.size(); - long end = query.noLimit() ? total : - Math.min(total, (long) start + query.limit()); - Set pageIds = CollectionUtil.subSet(allIds, start, (int) end); - if (pageIds.isEmpty()) { - return PageIds.EMPTY; - } - - int next = (int) end; - PageState pageState; - if (next < total) { - pageState = new PageState(new byte[]{1}, next, pageIds.size()); - } else { - pageState = new PageState(PageState.EMPTY_BYTES, 0, - pageIds.size()); - } - return new PageIds(pageIds, pageState); - } - - private List querySortedRangeIndexes(IndexLabel indexLabel, - ConditionQuery query) { - List indexes = new ArrayList<>(); - Iterator entries = null; - String spaceGraph = this.params() - .graph().spaceGraphName(); - LockUtil.Locks locks = new LockUtil.Locks(spaceGraph); - ConditionQuery scanQuery = query.copy(); - scanQuery.page(null); - scanQuery.offset(0L); - scanQuery.limit(Query.NO_LIMIT); - try { - locks.lockReads(LockUtil.INDEX_LABEL_DELETE, indexLabel.id()); - locks.lockReads(LockUtil.INDEX_LABEL_REBUILD, indexLabel.id()); - if (!indexLabel.system()) { - graph().indexLabel(indexLabel.id()); - } - - entries = super.query(scanQuery).iterator(); - while (entries.hasNext()) { - HugeIndex index = this.readMatchedIndex(indexLabel, scanQuery, - entries.next()); - if (index == null) { - continue; - } - this.removeExpiredIndexIfNeeded(index, scanQuery.showExpired()); - this.recordIndexValue(scanQuery, index); - indexes.add(index); - Query.checkForceCapacity(indexes.size()); - } - } finally { - locks.unlock(); - CloseableIterator.closeIterator(entries); - } - - Collections.sort(indexes, (a, b) -> { - return this.compareRangeIndexValues(a, b); - }); - return indexes; - } - - @SuppressWarnings({"rawtypes", "unchecked"}) - private int compareRangeIndexValues(HugeIndex left, HugeIndex right) { - Object leftValue = left.fieldValues(); - Object rightValue = right.fieldValues(); - E.checkArgument(leftValue instanceof Comparable, - "Invalid range index value '%s'", leftValue); - E.checkArgument(rightValue instanceof Comparable, - "Invalid range index value '%s'", rightValue); - return ((Comparable) leftValue).compareTo(rightValue); - } - - static class SortedRangeBatchIdHolder extends BatchIdHolder { - - private final List idList; - private int offset; - private PageIds pendingBatch; - - SortedRangeBatchIdHolder(ConditionQuery query, Set ids) { - super(query, Collections.emptyIterator(), batch -> { - throw new IllegalStateException("Unexpected sorted index fetcher"); - }); - this.idList = new ArrayList<>(ids); - this.offset = 0; - this.pendingBatch = null; - } - - @Override - public boolean keepOrder() { - return true; - } - - @Override - public boolean hasNext() { - if (this.pendingBatch != null) { - return true; - } - if (this.exhausted) { - return false; - } - return this.offset < this.idList.size(); - } - - @Override - public IdHolder next() { - if (!this.hasNext()) { - throw new java.util.NoSuchElementException(); - } - return this; - } - - @Override - public PageIds fetchNext(String page, long batchSize) { - E.checkArgument(page == null, - "Not support page parameter by BatchIdHolder"); - E.checkArgument(batchSize >= 0L, - "Invalid batch size value: %s", batchSize); - if (this.pendingBatch != null) { - PageIds result = this.pendingBatch; - this.pendingBatch = null; - return result; - } - return this.fetchBatch(batchSize); - } - - @Override - public Set all() { - Set allIds = InsertionOrderUtil.newSet(); - if (this.pendingBatch != null) { - allIds.addAll(this.pendingBatch.ids()); - } - if (this.offset < this.idList.size()) { - allIds.addAll(this.idList.subList(this.offset, - this.idList.size())); - } - this.close(); - return allIds; - } - - @Override - public PageIds peekNext(long size) { - E.checkArgument(this.pendingBatch == null, - "Can't call peekNext() twice"); - this.pendingBatch = this.fetchBatch(size); - return this.pendingBatch; - } - - @Override - public void close() { - this.exhausted = true; - this.pendingBatch = null; - this.offset = this.idList.size(); - } - - private PageIds fetchBatch(long batchSize) { - if (this.offset >= this.idList.size() || batchSize == 0L) { - this.close(); - return PageIds.EMPTY; - } - - int end; - if (batchSize == Query.NO_LIMIT) { - end = this.idList.size(); - } else { - end = (int) Math.min((long) this.idList.size(), - this.offset + batchSize); - } - Set batchIds = InsertionOrderUtil.newSet(); - batchIds.addAll(this.idList.subList(this.offset, end)); - this.offset = end; - this.exhausted = this.offset >= this.idList.size(); - return new PageIds(batchIds, PageState.EMPTY); - } - } - - private static class SortedRangePagingIdHolder extends PagingIdHolder { - - SortedRangePagingIdHolder(ConditionQuery query, - Function fetcher) { - super(query, fetcher); - } - - @Override - public boolean keepOrder() { - return true; - } - } - @Watched(prefix = "index") private IdHolder doIndexQueryBatch(IndexLabel indexLabel, ConditionQuery query) { @@ -947,7 +705,7 @@ private IdHolder doIndexQueryBatch(IndexLabel indexLabel, } finally { locks.unlock(); } - }); + }, this.keepBackendIndexOrder(indexLabel)); } private void recordIndexValue(ConditionQuery query, HugeIndex index) { diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java index 0abb6458b9..b85f01bb47 100755 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessions.java @@ -150,6 +150,18 @@ public abstract BackendColumnIterator scan(String table, int scanType, byte[] query); + public BackendColumnIterator scan(String table, + byte[] ownerKeyFrom, + byte[] ownerKeyTo, + byte[] keyFrom, + byte[] keyTo, + int scanType, + byte[] query, + long limit) { + return this.scan(table, ownerKeyFrom, ownerKeyTo, keyFrom, keyTo, + scanType, query); + } + public abstract BackendColumnIterator scan(String table, byte[] ownerKeyFrom, byte[] ownerKeyTo, @@ -159,6 +171,19 @@ public abstract BackendColumnIterator scan(String table, byte[] query, byte[] position); + public BackendColumnIterator scan(String table, + byte[] ownerKeyFrom, + byte[] ownerKeyTo, + byte[] keyFrom, + byte[] keyTo, + int scanType, + byte[] query, + byte[] position, + long limit) { + return this.scan(table, ownerKeyFrom, ownerKeyTo, keyFrom, keyTo, + scanType, query, position); + } + public abstract BackendColumnIterator scan(String table, int codeFrom, int codeTo, diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java index 2f98d03745..0e53786a79 100755 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreSessionsImpl.java @@ -691,6 +691,16 @@ public BackendColumnIterator scan(String table, byte[] ownerKeyFrom, byte[] ownerKeyTo, byte[] keyFrom, byte[] keyTo, int scanType, byte[] query) { + return this.scan(table, ownerKeyFrom, ownerKeyTo, keyFrom, keyTo, + scanType, query, HgStoreClientConst.NO_LIMIT); + } + + @Override + public BackendColumnIterator scan(String table, byte[] ownerKeyFrom, + byte[] ownerKeyTo, + byte[] keyFrom, byte[] keyTo, + int scanType, byte[] query, + long limit) { assert !this.hasChanges(); HgKvIterator result = this.graph.scanIterator(table, HgOwnerKey.of( @@ -699,7 +709,7 @@ public BackendColumnIterator scan(String table, byte[] ownerKeyFrom, HgOwnerKey.of( ownerKeyTo, keyTo), - 0, + toHstoreLimit(limit), scanType, query); return new ColumnIterator<>(table, result, keyFrom, keyTo, @@ -712,6 +722,17 @@ public BackendColumnIterator scan(String table, byte[] ownerKeyFrom, byte[] keyFrom, byte[] keyTo, int scanType, byte[] query, byte[] position) { + return this.scan(table, ownerKeyFrom, ownerKeyTo, keyFrom, keyTo, + scanType, query, position, + HgStoreClientConst.NO_LIMIT); + } + + @Override + public BackendColumnIterator scan(String table, byte[] ownerKeyFrom, + byte[] ownerKeyTo, + byte[] keyFrom, byte[] keyTo, + int scanType, byte[] query, + byte[] position, long limit) { assert !this.hasChanges(); HgKvIterator result = this.graph.scanIterator(table, HgOwnerKey.of( @@ -720,14 +741,23 @@ public BackendColumnIterator scan(String table, byte[] ownerKeyFrom, HgOwnerKey.of( ownerKeyTo, keyTo), - 0, + toHstoreLimit(limit), scanType, query); - result.seek(position); + if (position != null && position.length > 0) { + result.seek(position); + } return new ColumnIterator<>(table, result, keyFrom, keyTo, scanType); } + private long toHstoreLimit(long limit) { + if (limit <= 0L || limit == Query.NO_LIMIT) { + return HgStoreClientConst.NO_LIMIT; + } + return limit; + } + @Override public BackendColumnIterator scan(String table, int codeFrom, int codeTo, int scanType, diff --git a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java index e1830111c3..4ac885b177 100755 --- a/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java +++ b/hugegraph-server/hugegraph-hstore/src/main/java/org/apache/hugegraph/backend/store/hstore/HstoreTable.java @@ -625,9 +625,8 @@ protected BackendColumnIterator queryByRange(Session session, } ConditionQuery cq; Query origin = query.originQuery(); - byte[] position = null; if (query.paging() && !query.page().isEmpty()) { - position = PageState.fromString(query.page()).position(); + type = (type & ~Session.SCAN_GTE_BEGIN) | Session.SCAN_GT_BEGIN; } byte[] ownerStart = this.ownerByQueryDelegate.apply(query.resultType(), query.start()); @@ -636,6 +635,7 @@ protected BackendColumnIterator queryByRange(Session session, if (origin instanceof ConditionQuery && (query.resultType().isEdge() || query.resultType().isVertex())) { cq = (ConditionQuery) query.originQuery(); + long limit = rangeScanLimit(query); // LOG.debug("query {} with ownerKeyFrom: {}, ownerKeyTo: {}, " + // "keyFrom: {}, keyTo: {}, " + @@ -643,11 +643,18 @@ protected BackendColumnIterator queryByRange(Session session, // this.table(), bytes2String(ownerStart), // bytes2String(ownerEnd), bytes2String(start), // bytes2String(end), type, cq.bytes()); - return session.scan(this.table(), ownerStart, - ownerEnd, start, end, type, cq.bytes(), position); + return session.scan(this.table(), ownerStart, ownerEnd, start, + end, type, cq.bytes(), null, limit); + } + return session.scan(this.table(), ownerStart, ownerEnd, start, end, + type, null, null, rangeScanLimit(query)); + } + + private static long rangeScanLimit(IdRangeQuery query) { + if (query.noLimit()) { + return HgStoreClientConst.NO_LIMIT; } - return session.scan(this.table(), ownerStart, - ownerEnd, start, end, type, null, position); + return query.total(); } protected BackendColumnIterator queryByCond(Session session, diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java deleted file mode 100644 index a1b0d4e8c6..0000000000 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransactionTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.backend.tx; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import org.apache.hugegraph.backend.id.Id; -import org.apache.hugegraph.backend.id.IdGenerator; -import org.apache.hugegraph.backend.page.PageIds; -import org.apache.hugegraph.backend.query.ConditionQuery; -import org.apache.hugegraph.testutil.Assert; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.util.InsertionOrderUtil; -import org.junit.Test; - -import com.google.common.collect.ImmutableList; - -public class GraphIndexTransactionTest { - - @Test - public void testSortedRangeBatchHolderKeepsPeekedBatch() { - ConditionQuery query = new ConditionQuery(HugeType.VERTEX); - Id id1 = IdGenerator.of(1); - Id id2 = IdGenerator.of(2); - Id id3 = IdGenerator.of(3); - Set ids = InsertionOrderUtil.newSet(); - ids.add(id1); - ids.add(id2); - ids.add(id3); - - GraphIndexTransaction.SortedRangeBatchIdHolder holder = - new GraphIndexTransaction.SortedRangeBatchIdHolder(query, ids); - - Assert.assertTrue(holder.keepOrder()); - - PageIds peeked = holder.peekNext(2); - Assert.assertEquals(ImmutableList.of(id1, id2), asList(peeked.ids())); - - PageIds firstBatch = holder.fetchNext(null, 2); - Assert.assertEquals(ImmutableList.of(id1, id2), - asList(firstBatch.ids())); - - PageIds secondBatch = holder.fetchNext(null, 2); - Assert.assertEquals(ImmutableList.of(id3), asList(secondBatch.ids())); - Assert.assertFalse(holder.hasNext()); - } - - @Test - public void testSortedRangeBatchHolderClosesOnZeroBatch() { - ConditionQuery query = new ConditionQuery(HugeType.VERTEX); - Set ids = InsertionOrderUtil.newSet(); - ids.add(IdGenerator.of(1)); - - GraphIndexTransaction.SortedRangeBatchIdHolder holder = - new GraphIndexTransaction.SortedRangeBatchIdHolder(query, ids); - - PageIds batch = holder.fetchNext(null, 0); - Assert.assertTrue(batch.empty()); - Assert.assertFalse(holder.hasNext()); - } - - private static List asList(Set ids) { - return new ArrayList<>(ids); - } -} diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index d8f0b4c35b..967ca0049a 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -8775,6 +8775,27 @@ public void testQueryByRangeIndexInPage() { Assert.assertEquals(3, vertices2.size()); Assert.assertTrue(CollectionUtil.intersect(vertices1, vertices2) .isEmpty()); + + // There are 12 vertices matched, ordered by range-index key + iter = g.V().hasLabel("software") + .has("price", P.between(100, 400)) + .has("~page", "") + .limit(5); + + vertices1 = IteratorUtils.list(iter); + Assert.assertEquals(5, vertices1.size()); + assertSortedByPrice(vertices1); + page = TraversalUtil.page(iter); + vertices2 = g.V().hasLabel("software") + .has("price", P.between(100, 400)) + .has("~page", page).limit(5).toList(); + Assert.assertEquals(5, vertices2.size()); + assertSortedByPrice(vertices2); + Assert.assertTrue((int) vertices1.get(vertices1.size() - 1) + .value("price") <= + (int) vertices2.get(0).value("price")); + Assert.assertTrue(CollectionUtil.intersect(vertices1, vertices2) + .isEmpty()); } @Test @@ -9603,6 +9624,15 @@ private Vertex vertex(String label, String pkName, Object pkValue) { return vertices.size() == 1 ? vertices.get(0) : null; } + private static void assertSortedByPrice(List vertices) { + int previous = Integer.MIN_VALUE; + for (Vertex vertex : vertices) { + int current = vertex.value("price"); + Assert.assertTrue(previous <= current); + previous = current; + } + } + private static void assertContains(List vertices, Object... keyValues) { Assert.assertTrue(Utils.contains(vertices, new FakeObjects.FakeVertex(keyValues))); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index 9935909a02..3c856937bd 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java @@ -17,7 +17,6 @@ package org.apache.hugegraph.unit; -import org.apache.hugegraph.backend.tx.GraphIndexTransactionTest; import org.apache.hugegraph.core.RoleElectionStateMachineTest; import org.apache.hugegraph.meta.MetaManagerSchemaCacheClearEventTest; import org.apache.hugegraph.traversal.optimize.TraversalUtilOptimizeTest; @@ -123,7 +122,6 @@ BackendMutationTest.class, ConditionTest.class, ConditionQueryFlattenTest.class, - GraphIndexTransactionTest.class, QueryTest.class, QueryResultsTest.class, RangeTest.class, diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxSessionProxy.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxSessionProxy.java index 65e3b76ec0..401e415e8b 100644 --- a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxSessionProxy.java +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/NodeTxSessionProxy.java @@ -434,7 +434,7 @@ public HgKvIterator scanIterator(String table, HgOwnerKey startKey, HgAssert.isFalse(startKey == null, "The argument is invalid: startKey"); HgAssert.isFalse(endKey == null, "The argument is invalid: endKey"); - return this.toHgKvIteratorProxy( + return this.toOrderedHgKvIteratorProxy( this.toNodeTkvList(table, startKey, endKey) .parallelStream() .map( @@ -452,7 +452,7 @@ public HgKvIterator scanIterator(String table, HgOwnerKey startKey, H HgAssert.isFalse(startKey == null, "The argument is invalid: startKey"); HgAssert.isFalse(endKey == null, "The argument is invalid: endKey"); - return this.toHgKvIteratorProxy( + return this.toOrderedHgKvIteratorProxy( this.toNodeTkvList(table, startKey, endKey) .parallelStream() .map( @@ -472,7 +472,7 @@ public HgKvIterator scanIterator(String table, HgOwnerKey startKey, H HgAssert.isFalse(startKey == null, "The argument is invalid: startKey"); HgAssert.isFalse(endKey == null, "The argument is invalid: endKey"); - return this.toHgKvIteratorProxy( + return this.toOrderedHgKvIteratorProxy( this.toNodeTkvList(table, startKey, endKey) .parallelStream() .map( @@ -642,6 +642,15 @@ private BiFunction, HgScanQuery.ScanBuilder> toScanQueryFu } /*-- common --*/ + @SuppressWarnings("unchecked") + private HgKvIterator toOrderedHgKvIteratorProxy(List iteratorList, + long limit) { + List> iterators = + (List>) + (List) iteratorList; + return new OrderedKvIterator(iterators, limit); + } + private HgKvIterator toHgKvIteratorProxy(List iteratorList, long limit) { boolean isAllOrderedLimiter = iteratorList.stream() .allMatch( diff --git a/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/OrderedKvIterator.java b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/OrderedKvIterator.java new file mode 100644 index 0000000000..734829401b --- /dev/null +++ b/hugegraph-store/hg-store-client/src/main/java/org/apache/hugegraph/store/client/OrderedKvIterator.java @@ -0,0 +1,150 @@ +/* + * 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.store.client; + +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.PriorityQueue; + +import org.apache.hugegraph.store.HgKvEntry; +import org.apache.hugegraph.store.HgKvIterator; +import org.apache.hugegraph.store.client.util.HgStoreClientConst; + +final class OrderedKvIterator implements HgKvIterator { + + private final List> iterators; + private final PriorityQueue queue; + private final long limit; + + private boolean initialized; + private long count; + private HgKvEntry current; + private byte[] position; + private byte[] seekPosition; + + OrderedKvIterator(List> iterators, + long limit) { + this.iterators = iterators; + this.queue = new PriorityQueue<>((left, right) -> { + int result = Arrays.compareUnsigned(left.entry.key(), + right.entry.key()); + if (result != 0) { + return result; + } + return Integer.compare(left.source, right.source); + }); + this.limit = limit <= HgStoreClientConst.NO_LIMIT ? Long.MAX_VALUE : + limit; + this.initialized = false; + this.count = 0L; + this.current = null; + this.position = HgStoreClientConst.EMPTY_BYTES; + this.seekPosition = HgStoreClientConst.EMPTY_BYTES; + } + + @Override + public boolean hasNext() { + this.initialize(); + return this.count < this.limit && !this.queue.isEmpty(); + } + + @Override + public HgKvEntry next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + + SourceEntry entry = this.queue.poll(); + this.current = entry.entry; + this.position = entry.entry.key(); + this.count++; + + HgKvIterator iterator = + this.iterators.get(entry.source); + this.addNext(entry.source, iterator); + return this.current; + } + + @Override + public byte[] key() { + return this.current == null ? null : this.current.key(); + } + + @Override + public byte[] value() { + return this.current == null ? null : this.current.value(); + } + + @Override + public byte[] position() { + return this.position; + } + + @Override + public void seek(byte[] position) { + if (this.initialized) { + throw new IllegalStateException("Can't seek after reading"); + } + this.seekPosition = position == null ? HgStoreClientConst.EMPTY_BYTES : + position; + } + + @Override + public void close() { + for (HgKvIterator iterator : this.iterators) { + iterator.close(); + } + this.queue.clear(); + } + + private void initialize() { + if (this.initialized) { + return; + } + for (int i = 0; i < this.iterators.size(); i++) { + HgKvIterator iterator = + this.iterators.get(i); + this.addNext(i, iterator); + } + this.initialized = true; + } + + private void addNext(int source, + HgKvIterator iterator) { + while (iterator.hasNext()) { + HgKvEntry entry = iterator.next(); + if (this.seekPosition.length == 0 || + Arrays.compareUnsigned(entry.key(), this.seekPosition) > 0) { + this.queue.add(new SourceEntry(source, entry)); + break; + } + } + } + + private static final class SourceEntry { + + private final int source; + private final HgKvEntry entry; + + private SourceEntry(int source, HgKvEntry entry) { + this.source = source; + this.entry = entry; + } + } +} diff --git a/hugegraph-store/hg-store-client/src/test/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java b/hugegraph-store/hg-store-client/src/test/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java new file mode 100644 index 0000000000..742e3e8a5f --- /dev/null +++ b/hugegraph-store/hg-store-client/src/test/java/org/apache/hugegraph/store/client/OrderedKvIteratorTest.java @@ -0,0 +1,173 @@ +/* + * 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.store.client; + +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.hugegraph.store.HgKvEntry; +import org.apache.hugegraph.store.HgKvIterator; +import org.junit.Assert; +import org.junit.Test; + +public class OrderedKvIteratorTest { + + @Test + public void testMergeByKeyWithLimit() { + OrderedKvIterator iterator = new OrderedKvIterator(Arrays.asList( + new TestIterator(1, 4), + new TestIterator(2, 3) + ), 3L); + + Assert.assertTrue(iterator.hasNext()); + Assert.assertEquals(1, key(iterator.next())); + Assert.assertArrayEquals(keyBytes(1), iterator.position()); + + Assert.assertTrue(iterator.hasNext()); + Assert.assertEquals(2, key(iterator.next())); + Assert.assertArrayEquals(keyBytes(2), iterator.position()); + + Assert.assertTrue(iterator.hasNext()); + Assert.assertEquals(3, key(iterator.next())); + Assert.assertArrayEquals(keyBytes(3), iterator.position()); + + Assert.assertFalse(iterator.hasNext()); + } + + @Test + public void testNoLimitReturnsAllKeysInOrder() { + OrderedKvIterator iterator = new OrderedKvIterator(Arrays.asList( + new TestIterator(1, 5), + new TestIterator(2, 4) + ), 0L); + + Assert.assertEquals(1, key(iterator.next())); + Assert.assertEquals(2, key(iterator.next())); + Assert.assertEquals(4, key(iterator.next())); + Assert.assertEquals(5, key(iterator.next())); + Assert.assertFalse(iterator.hasNext()); + } + + @Test + public void testSeekBeforeReadSkipsBoundaryKey() { + OrderedKvIterator iterator = new OrderedKvIterator(Arrays.asList( + new TestIterator(1, 4), + new TestIterator(2, 3) + ), 0L); + + iterator.seek(keyBytes(3)); + + Assert.assertEquals(4, key(iterator.next())); + Assert.assertFalse(iterator.hasNext()); + } + + @Test + public void testSeekDoesNotSkipLowerSourceWithLargerKeys() { + OrderedKvIterator iterator = new OrderedKvIterator(Arrays.asList( + new TestIterator(1, 100), + new TestIterator(2, 3) + ), 0L); + + iterator.seek(keyBytes(2)); + + Assert.assertEquals(3, key(iterator.next())); + Assert.assertEquals(100, key(iterator.next())); + Assert.assertFalse(iterator.hasNext()); + } + + private static int key(HgKvEntry entry) { + return entry.key()[0] & 0xFF; + } + + private static byte[] keyBytes(int key) { + return new byte[]{(byte) key}; + } + + private static final class TestIterator implements HgKvIterator { + + private final List keys; + private int offset; + private HgKvEntry current; + + private TestIterator(Integer... keys) { + this.keys = Arrays.asList(keys); + this.offset = 0; + this.current = null; + } + + @Override + public boolean hasNext() { + return this.offset < this.keys.size(); + } + + @Override + public HgKvEntry next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + int key = this.keys.get(this.offset++); + this.current = new TestEntry(keyBytes(key)); + return this.current; + } + + @Override + public byte[] key() { + return this.current == null ? null : this.current.key(); + } + + @Override + public byte[] value() { + return this.current == null ? null : this.current.value(); + } + + @Override + public byte[] position() { + byte[] key = this.key(); + return key == null ? null : new byte[]{(byte) 0xFF, key[0]}; + } + + @Override + public void seek(byte[] position) { + int target = position[0] & 0xFF; + while (this.offset < this.keys.size() && + this.keys.get(this.offset) < target) { + this.offset++; + } + } + } + + private static final class TestEntry implements HgKvEntry { + + private final byte[] key; + + private TestEntry(byte[] key) { + this.key = key; + } + + @Override + public byte[] key() { + return this.key; + } + + @Override + public byte[] value() { + return this.key; + } + } +}