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/ConditionQuery.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/query/ConditionQuery.java index 063d23aa6d..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 @@ -256,24 +256,27 @@ public boolean containsLabelOrUserpropRelation() { return false; } + /** + * Returns the legacy condition value of the specified key. + * + * This method keeps the historical behavior for existing callers: + *
    + *
  • returns {@code null} if no top-level EQ/IN relation exists
  • + *
  • returns {@code null} if top-level EQ/IN relations resolve to empty
  • + *
  • returns the single value if only one value is resolved
  • + *
  • returns the raw IN list if there is exactly one top-level IN relation
  • + *
  • throws if multiple values remain after resolving several relations
  • + *
+ * + * Prefer {@link #conditionValues(Object)}, {@link #uniqueConditionValue(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; } @@ -288,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; @@ -323,20 +305,151 @@ 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 #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(); + List valuesIN = InsertionOrderUtil.newList(); + this.collectConditionValues(key, valuesEQ, valuesIN); + if (valuesEQ.isEmpty() && valuesIN.isEmpty()) { + return InsertionOrderUtil.newSet(); + } + 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. + * + * 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; + } + + /** + * 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)); } 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) { 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) { @@ -566,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/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/serializer/BinarySerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/BinarySerializer.java index 0bb07760a5..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.condition(HugeKeys.LABEL); + Id label = (Id) this.edgeIdConditionValue(cq, HugeKeys.LABEL); BytesBuffer start = BytesBuffer.allocate(BytesBuffer.BUF_EDGE_ID); writePartitionedId(HugeType.EDGE, vertex, start); @@ -722,7 +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 = cq.condition(key); + Object value = this.edgeIdConditionValue(cq, key); if (value != null) { count++; @@ -763,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 2d5cb81ec1..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.condition(HugeKeys.LABEL); + Object label = this.edgeIdConditionValue(cq, HugeKeys.LABEL); List start = new ArrayList<>(cq.conditionsSize()); start.add(writeEntryId((Id) vertex)); @@ -491,7 +491,7 @@ private Query writeQueryEdgePrefixCondition(ConditionQuery cq) { List condParts = new ArrayList<>(cq.conditionsSize()); for (HugeKeys key : EdgeId.KEYS) { - Object value = cq.condition(key); + Object value = this.edgeIdConditionValue(cq, key); if (value == null) { break; } @@ -516,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 0e2c58bddc..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 = cq.condition(HugeKeys.LABEL); + 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 = query.condition(HugeKeys.LABEL); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (label == null) { label = IdGenerator.ZERO; } 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..2b8d02e581 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) @@ -415,8 +416,11 @@ 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; + // 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); HugeType indexType; SchemaLabel schemaLabel; @@ -482,7 +486,7 @@ private IdHolderList queryByUserprop(ConditionQuery query) { } Set indexes = this.collectMatchedIndexes(query); if (indexes.isEmpty()) { - Id label = query.condition(HugeKeys.LABEL); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); throw noIndexException(this.graph(), query, label); } @@ -682,8 +686,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()); @@ -724,8 +731,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())) { @@ -753,14 +763,48 @@ 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(); - Id label = query.condition(HugeKeys.LABEL); + boolean hasLabelValues = query.containsConditionValues(HugeKeys.LABEL); + Set labels = query.conditionValues(HugeKeys.LABEL); List schemaLabels; - if (label != null) { - // Query has LABEL condition + if (hasLabelValues && labels.isEmpty()) { + // LABEL EQ/IN conditions resolve to an empty intersection. + 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 +817,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()) { @@ -945,7 +990,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(); @@ -986,7 +1031,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(); @@ -1554,8 +1599,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; } @@ -1781,7 +1831,7 @@ protected long removeIndexLeft(ConditionQuery query, } // Check label is matched - Id label = query.condition(HugeKeys.LABEL); + 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)) { 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..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); } @@ -1059,7 +1060,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 = cq.uniqueConditionValue(HugeKeys.LABEL); if (this.storeFeatures().supportsFatherAndSubEdgeLabel() && label != null && graph().edgeLabel(label).isFather() && @@ -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) { @@ -1389,7 +1396,7 @@ private static boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (label == null) { return false; } @@ -1522,7 +1529,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 = query.uniqueConditionValue(HugeKeys.LABEL); // Optimize vertex query if (label != null && query.resultType().isVertex()) { @@ -1914,7 +1921,8 @@ private boolean rightResultFromIndexQuery(Query query, HugeElement elem) { } ConditionQuery cq = (ConditionQuery) query; - if (cq.condition(HugeKeys.LABEL) != null && cq.resultType().isEdge()) { + if (cq.uniqueConditionValue(HugeKeys.LABEL) != null && + cq.resultType().isEdge()) { if (cq.conditions().size() == 1) { // g.E().hasLabel(xxx) return true; 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..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,7 +582,9 @@ private void fillFilterBySortKeys(Query query, Id[] edgeLabels, ConditionQuery condQuery = (ConditionQuery) query; if (!GraphTransaction.matchFullEdgeSortKeys(condQuery, this.graph())) { - Id label = condQuery.condition(HugeKeys.LABEL); + // 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'", this.graph().mapPkId2Name(properties.keySet()), 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-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..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 @@ -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 = 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 || @@ -459,7 +459,7 @@ private boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = query.uniqueConditionValue(HugeKeys.LABEL); if (label == null) { return false; } 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..20cb66bb82 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 @@ -114,15 +114,12 @@ private static boolean direction(Condition condition) { protected static BackendEntryIterator newEntryIterator( BackendColumnIterator cols, Query query) { - return new BinaryEntryIterator<>(cols, query, (entry, col) -> { - if (entry == null || !entry.belongToMe(col)) { - HugeType type = query.resultType(); - // NOTE: only support BinaryBackendEntry currently - entry = new BinaryBackendEntry(type, col.name); - } - entry.columns(col); - return entry; - }); + BiFunction merger = + (entry, col) -> mergeColumn(query, entry, col); + if (query.resultType().isRangeIndex()) { + return new RangeIndexEntryIterator(cols, query, merger); + } + return new BinaryEntryIterator<>(cols, query, merger); } protected static BackendEntryIterator newEntryIteratorOlap( @@ -138,6 +135,42 @@ protected static BackendEntryIterator newEntryIteratorOlap( }); } + private static BackendEntry mergeColumn(Query query, BackendEntry entry, + BackendColumn col) { + if (entry == null || !entry.belongToMe(col)) { + HugeType type = query.resultType(); + // NOTE: only support BinaryBackendEntry currently + entry = new BinaryBackendEntry(type, col.name); + } + entry.columns(col); + return entry; + } + + private static final class RangeIndexEntryIterator + extends BinaryEntryIterator { + + private byte[] lastPosition; + + private RangeIndexEntryIterator( + BackendColumnIterator cols, Query query, + BiFunction merger) { + super(cols, query, merger); + this.lastPosition = PageState.EMPTY_BYTES; + } + + @Override + public BackendEntry next() { + BackendEntry entry = super.next(); + this.lastPosition = entry.id().asBytes(); + return entry; + } + + @Override + protected PageState pageState() { + return new PageState(this.lastPosition, 0, (int) this.count()); + } + } + public static String bytes2String(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { @@ -615,6 +648,11 @@ private boolean onlyOwnerVertex(Condition condition) { protected BackendColumnIterator queryByRange(Session session, IdRangeQuery query) { + /* + * FIXME: multi-partition HStore range-index paging still needs a + * bounded ordered merge in the store-client layer before it can + * guarantee globally ordered pages. + */ byte[] start = query.start().asBytes(); byte[] end = query.end() == null ? null : query.end().asBytes(); int type = query.inclusiveStart() ? @@ -626,13 +664,13 @@ 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(); - } byte[] ownerStart = this.ownerByQueryDelegate.apply(query.resultType(), query.start()); byte[] ownerEnd = this.ownerByQueryDelegate.apply(query.resultType(), query.end()); + if (query.paging() && !query.page().isEmpty()) { + position = PageState.fromString(query.page()).position(); + } if (origin instanceof ConditionQuery && (query.resultType().isEdge() || query.resultType().isVertex())) { cq = (ConditionQuery) query.originQuery(); @@ -643,11 +681,11 @@ 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(), position); } - return session.scan(this.table(), ownerStart, - ownerEnd, start, end, type, null, position); + return session.scan(this.table(), ownerStart, ownerEnd, start, end, + type, null, position); } protected BackendColumnIterator queryByCond(Session session, diff --git a/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java b/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java new file mode 100644 index 0000000000..c11cc37b9a --- /dev/null +++ b/hugegraph-server/hugegraph-hstore/src/test/java/org/apache/hugegraph/backend/store/hstore/HstoreTableTest.java @@ -0,0 +1,166 @@ +/* + * 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.store.hstore; + +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.hugegraph.backend.id.Id.IdType; +import org.apache.hugegraph.backend.id.IdGenerator; +import org.apache.hugegraph.backend.page.PageInfo; +import org.apache.hugegraph.backend.page.PageState; +import org.apache.hugegraph.backend.query.IdRangeQuery; +import org.apache.hugegraph.backend.query.Query; +import org.apache.hugegraph.backend.store.BackendEntry; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumn; +import org.apache.hugegraph.backend.store.BackendEntry.BackendColumnIterator; +import org.apache.hugegraph.backend.store.BackendEntryIterator; +import org.apache.hugegraph.type.HugeType; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class HstoreTableTest { + + @Test + public void testRangeIndexPageStateIgnoresPrefetchedColumn() { + Query query = new Query(HugeType.RANGE_INT_INDEX); + query.page(""); + query.limit(1L); + + BackendEntryIterator iterator = HstoreTable.newEntryIterator( + new TestColumnIterator(1, 2), query); + + Assert.assertTrue(iterator.hasNext()); + BackendEntry entry = iterator.next(); + Assert.assertArrayEquals(keyBytes(1), entry.id().asBytes()); + + PageState pageState = PageInfo.pageState(iterator); + Assert.assertArrayEquals(keyBytes(1), pageState.position()); + Assert.assertEquals(1L, pageState.total()); + } + + @Test + public void testQueryByRangeUsesLegacyScanForRangeIndexQueries() { + HstoreTable table = new HstoreTable("graph", "index"); + HstoreSessions.Session session = Mockito.mock( + HstoreSessions.Session.class); + + IdRangeQuery query = rangeIndexQuery(); + table.queryByRange(session, query); + verifyLegacyRangeScan(session); + + Mockito.reset(session); + query = rangeIndexQuery(); + query.limit(10L); + table.queryByRange(session, query); + verifyLegacyRangeScan(session); + + Mockito.reset(session); + query = rangeIndexQuery(); + query.offset(1L); + table.queryByRange(session, query); + verifyLegacyRangeScan(session); + + Mockito.reset(session); + byte[] pagePosition = keyBytes(3); + query = rangeIndexQuery(); + query.page(new PageState(pagePosition, 0, 1).toString()); + query.limit(10L); + table.queryByRange(session, query); + verifyLegacyRangeScan(session, pagePosition); + } + + private static IdRangeQuery rangeIndexQuery() { + return new IdRangeQuery(HugeType.RANGE_INT_INDEX, null, + IdGenerator.of(keyBytes(1), IdType.STRING), + true, + IdGenerator.of(keyBytes(9), IdType.STRING), + false); + } + + private static byte[] keyBytes(int key) { + byte[] bytes = new byte[9]; + bytes[0] = HugeType.RANGE_INT_INDEX.code(); + bytes[8] = (byte) key; + return bytes; + } + + private static final class TestColumnIterator + implements BackendColumnIterator { + + private final List keys; + private int offset; + private byte[] position; + + private TestColumnIterator(Integer... keys) { + this.keys = Arrays.asList(keys); + this.offset = 0; + this.position = null; + } + + @Override + public boolean hasNext() { + return this.offset < this.keys.size(); + } + + @Override + public BackendColumn next() { + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + byte[] key = keyBytes(this.keys.get(this.offset++)); + this.position = key; + return BackendColumn.of(key, key); + } + + @Override + public void close() { + // pass + } + + @Override + public byte[] position() { + return this.position; + } + } + + private static void verifyLegacyRangeScan(HstoreSessions.Session session) { + verifyLegacyRangeScan(session, null); + } + + private static void verifyLegacyRangeScan(HstoreSessions.Session session, + byte[] position) { + Mockito.verify(session).scan(Mockito.anyString(), + Mockito.any(byte[].class), + Mockito.any(byte[].class), + Mockito.any(byte[].class), + Mockito.any(byte[].class), + Mockito.anyInt(), + Mockito.isNull(), + positionMatcher(position)); + } + + private static byte[] positionMatcher(byte[] position) { + if (position == null) { + return Mockito.isNull(); + } + return Mockito.argThat(value -> Arrays.equals(position, value)); + } +} 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..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 @@ -3582,6 +3582,57 @@ 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 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 @@ -7691,6 +7742,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(); 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..4746826ee8 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; @@ -4414,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])) @@ -8766,6 +8775,7 @@ public void testQueryByRangeIndexInPage() { Assert.assertEquals(3, vertices2.size()); Assert.assertTrue(CollectionUtil.intersect(vertices1, vertices2) .isEmpty()); + } @Test @@ -9076,6 +9086,76 @@ 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 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(); + initPersonIndex(true); + + 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/UnitTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/UnitTestSuite.java index fb7f0e744b..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 @@ -40,6 +40,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; @@ -122,6 +123,7 @@ ConditionTest.class, ConditionQueryFlattenTest.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; + } + } +} 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..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 @@ -48,6 +48,19 @@ public void testOrderBy() { query.orders()); } + @Test + 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)); + } + @Test public void testConditionWithEqAndIn() { Id label1 = IdGenerator.of(1); @@ -58,9 +71,37 @@ public void testConditionWithEqAndIn() { query.query(Condition.in(HugeKeys.LABEL, 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)); } + @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.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'", + e.getMessage())); + Assert.assertEquals(ImmutableList.of(label1, label2), + query.condition(HugeKeys.LABEL)); + } + @Test public void testConditionWithConflictingEqAndIn() { Id label1 = IdGenerator.of(1); @@ -73,9 +114,44 @@ public void testConditionWithConflictingEqAndIn() { query.query(Condition.in(HugeKeys.LABEL, 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); @@ -89,6 +165,15 @@ public void testConditionWithMultipleMatchedInValues() { query.query(Condition.in(HugeKeys.LABEL, 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'", + e.getMessage())); Assert.assertThrows(IllegalStateException.class, () -> query.condition(HugeKeys.LABEL), e -> Assert.assertContains("Illegal key 'LABEL'",