From ac25ec585d4914dffd7358ea3a1374eb613fe30f Mon Sep 17 00:00:00 2001 From: contrueCT Date: Sun, 12 Apr 2026 21:39:46 +0800 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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; + } + } +}