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..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 @@ -415,8 +415,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 +485,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); } @@ -648,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 { @@ -657,6 +663,250 @@ 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 SortedRangePagingIdHolder(query, q -> { + return this.querySortedRangeIndexPage(indexLabel, q); + }); + } + + private BatchIdHolder newSortedRangeIndexBatchHolder(ConditionQuery query, + Set ids) { + return new SortedRangeBatchIdHolder(query, ids); + } + + private Set querySortedRangeIndexIds(IndexLabel indexLabel, + ConditionQuery query) { + List indexes = this.querySortedRangeIndexes(indexLabel, + query); + Set ids = InsertionOrderUtil.newSet(); + for (HugeIndex index : indexes) { + ids.addAll(index.elementIds()); + Query.checkForceCapacity(ids.size()); + } + return ids; + } + + private PageIds querySortedRangeIndexPage(IndexLabel indexLabel, + ConditionQuery query) { + List indexes = this.querySortedRangeIndexes(indexLabel, + query); + Set allIds = InsertionOrderUtil.newSet(); + for (HugeIndex index : indexes) { + allIds.addAll(index.elementIds()); + Query.checkForceCapacity(allIds.size()); + } + if (allIds.isEmpty()) { + return PageIds.EMPTY; + } + + int start = 0; + if (!query.page().isEmpty()) { + start = PageState.fromString(query.page()).offset(); + } + if (start >= allIds.size()) { + return PageIds.EMPTY; + } + + long total = allIds.size(); + long end = query.noLimit() ? total : + Math.min(total, (long) start + query.limit()); + Set pageIds = CollectionUtil.subSet(allIds, start, (int) end); + if (pageIds.isEmpty()) { + return PageIds.EMPTY; + } + + int next = (int) end; + PageState pageState; + if (next < total) { + pageState = new PageState(new byte[]{1}, next, pageIds.size()); + } else { + pageState = new PageState(PageState.EMPTY_BYTES, 0, + pageIds.size()); + } + return new PageIds(pageIds, pageState); + } + + private List querySortedRangeIndexes(IndexLabel indexLabel, + ConditionQuery query) { + List indexes = new ArrayList<>(); + Iterator entries = null; + String spaceGraph = this.params() + .graph().spaceGraphName(); + LockUtil.Locks locks = new LockUtil.Locks(spaceGraph); + ConditionQuery scanQuery = query.copy(); + scanQuery.page(null); + scanQuery.offset(0L); + scanQuery.limit(Query.NO_LIMIT); + try { + locks.lockReads(LockUtil.INDEX_LABEL_DELETE, indexLabel.id()); + locks.lockReads(LockUtil.INDEX_LABEL_REBUILD, indexLabel.id()); + if (!indexLabel.system()) { + graph().indexLabel(indexLabel.id()); + } + + entries = super.query(scanQuery).iterator(); + while (entries.hasNext()) { + HugeIndex index = this.readMatchedIndex(indexLabel, scanQuery, + entries.next()); + if (index == null) { + continue; + } + this.removeExpiredIndexIfNeeded(index, scanQuery.showExpired()); + this.recordIndexValue(scanQuery, index); + indexes.add(index); + Query.checkForceCapacity(indexes.size()); + } + } finally { + locks.unlock(); + CloseableIterator.closeIterator(entries); + } + + Collections.sort(indexes, (a, b) -> { + return this.compareRangeIndexValues(a, b); + }); + return indexes; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private int compareRangeIndexValues(HugeIndex left, HugeIndex right) { + Object leftValue = left.fieldValues(); + Object rightValue = right.fieldValues(); + E.checkArgument(leftValue instanceof Comparable, + "Invalid range index value '%s'", leftValue); + E.checkArgument(rightValue instanceof Comparable, + "Invalid range index value '%s'", rightValue); + return ((Comparable) leftValue).compareTo(rightValue); + } + + static class SortedRangeBatchIdHolder extends BatchIdHolder { + + private final List idList; + private int offset; + private PageIds pendingBatch; + + SortedRangeBatchIdHolder(ConditionQuery query, Set ids) { + super(query, Collections.emptyIterator(), batch -> { + throw new IllegalStateException("Unexpected sorted index fetcher"); + }); + this.idList = new ArrayList<>(ids); + this.offset = 0; + this.pendingBatch = null; + } + + @Override + public boolean keepOrder() { + return true; + } + + @Override + public boolean hasNext() { + if (this.pendingBatch != null) { + return true; + } + if (this.exhausted) { + return false; + } + return this.offset < this.idList.size(); + } + + @Override + public IdHolder next() { + if (!this.hasNext()) { + throw new java.util.NoSuchElementException(); + } + return this; + } + + @Override + public PageIds fetchNext(String page, long batchSize) { + E.checkArgument(page == null, + "Not support page parameter by BatchIdHolder"); + E.checkArgument(batchSize >= 0L, + "Invalid batch size value: %s", batchSize); + if (this.pendingBatch != null) { + PageIds result = this.pendingBatch; + this.pendingBatch = null; + return result; + } + return this.fetchBatch(batchSize); + } + + @Override + public Set all() { + Set allIds = InsertionOrderUtil.newSet(); + if (this.pendingBatch != null) { + allIds.addAll(this.pendingBatch.ids()); + } + if (this.offset < this.idList.size()) { + allIds.addAll(this.idList.subList(this.offset, + this.idList.size())); + } + this.close(); + return allIds; + } + + @Override + public PageIds peekNext(long size) { + E.checkArgument(this.pendingBatch == null, + "Can't call peekNext() twice"); + this.pendingBatch = this.fetchBatch(size); + return this.pendingBatch; + } + + @Override + public void close() { + this.exhausted = true; + this.pendingBatch = null; + this.offset = this.idList.size(); + } + + private PageIds fetchBatch(long batchSize) { + if (this.offset >= this.idList.size() || batchSize == 0L) { + this.close(); + return PageIds.EMPTY; + } + + int end; + if (batchSize == Query.NO_LIMIT) { + end = this.idList.size(); + } else { + end = (int) Math.min((long) this.idList.size(), + this.offset + batchSize); + } + Set batchIds = InsertionOrderUtil.newSet(); + batchIds.addAll(this.idList.subList(this.offset, end)); + this.offset = end; + this.exhausted = this.offset >= this.idList.size(); + return new PageIds(batchIds, PageState.EMPTY); + } + } + + private static class SortedRangePagingIdHolder extends PagingIdHolder { + + SortedRangePagingIdHolder(ConditionQuery query, + Function fetcher) { + super(query, fetcher); + } + + @Override + public boolean keepOrder() { + return true; + } + } + @Watched(prefix = "index") private IdHolder doIndexQueryBatch(IndexLabel indexLabel, ConditionQuery query) { @@ -682,8 +932,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 +977,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 +1009,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 +1063,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 +1236,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 +1277,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 +1845,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 +2077,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-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-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/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(); 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..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 @@ -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])) @@ -9076,6 +9085,62 @@ 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(); + 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..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; + } + } +} 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'",