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 097e98df19..947cd05c91 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,32 @@ public boolean containsLabelOrUserpropRelation() { return false; } + /** + * Returns the legacy condition value of the specified key. + * + * This method keeps the historical behavior for existing callers: + * + * + * Prefer {@link #conditionValues(Object)}, + * {@link #singleConditionValueOrNull(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 +296,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 +310,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 singleConditionValueOrNull(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) { diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java index 734cb90426..999382ef03 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/serializer/AbstractSerializer.java @@ -27,6 +27,7 @@ import org.apache.hugegraph.config.HugeConfig; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.tinkerpop.gremlin.structure.Edge; public abstract class AbstractSerializer @@ -54,6 +55,18 @@ protected BackendEntry convertEntry(BackendEntry entry) { protected abstract Query writeQueryCondition(Query query); + protected Object edgeIdConditionValue(ConditionQuery query, + HugeKeys key) { + if (key == HugeKeys.LABEL) { + /* + * LABEL may still be represented by multiple top-level EQ/IN + * relations before strict edge-id serialization. + */ + return query.conditionValue(key); + } + return query.condition(key); + } + @Override public Query writeQuery(Query query) { HugeType type = query.resultType(); 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..057ea2ac71 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++; 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..dedf4bcab5 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; } 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..0093204374 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.singleConditionValueOrNull(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.singleConditionValueOrNull(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 6faace9671..f417962d7c 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.singleConditionValueOrNull(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; @@ -480,14 +484,18 @@ private IdHolderList queryByUserprop(ConditionQuery query) { } } } + boolean paging = query.paging(); + if (query.containsConditionValues(HugeKeys.LABEL) && + query.conditionValues(HugeKeys.LABEL).isEmpty()) { + return IdHolderList.empty(paging); + } Set indexes = this.collectMatchedIndexes(query); if (indexes.isEmpty()) { - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); throw noIndexException(this.graph(), query, label); } // Value type of Condition not matched - boolean paging = query.paging(); if (!validQueryConditionValues(this.graph(), query)) { return IdHolderList.empty(paging); } @@ -768,11 +776,17 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, @Watched(prefix = "index") private Set collectMatchedIndexes(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); - Id label = query.condition(HugeKeys.LABEL); + boolean 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); @@ -785,7 +799,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()) { @@ -1793,7 +1808,7 @@ protected long removeIndexLeft(ConditionQuery query, } // Check label is matched - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(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 0c962b11a2..4591765aa6 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 @@ -1056,7 +1056,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.singleConditionValueOrNull(HugeKeys.LABEL); if (this.storeFeatures().supportsFatherAndSubEdgeLabel() && label != null && graph().edgeLabel(label).isFather() && @@ -1386,7 +1386,7 @@ private static boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); if (label == null) { return false; } @@ -1519,7 +1519,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.singleConditionValueOrNull(HugeKeys.LABEL); // Optimize vertex query if (label != null && query.resultType().isVertex()) { @@ -1911,7 +1911,8 @@ private boolean rightResultFromIndexQuery(Query query, HugeElement elem) { } ConditionQuery cq = (ConditionQuery) query; - if (cq.condition(HugeKeys.LABEL) != null && cq.resultType().isEdge()) { + if (cq.singleConditionValueOrNull(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 fe8fa05687..d7ebf36839 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 @@ -602,7 +602,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 e6a56027a1..9936339c5a 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 @@ -173,6 +173,9 @@ public static void trySetGraph(Step step, HugeGraph graph) { public static void extractHasContainer(HugeGraphStep newStep, Traversal.Admin traversal) { + if (hasUnsafeLabelInTraversal(traversal, newStep)) { + return; + } Step step = newStep.getNextStep(); while (step instanceof HasStep || step instanceof NoOpBarrierStep) { Step nextStep = step.getNextStep(); @@ -260,13 +263,8 @@ private static boolean followedByMatchStep(Step step) { return null; } - List labels = new ArrayList<>(); - for (Traversal.Admin child : orStep.getLocalChildren()) { - if (!collectPositiveLabelValues(child, labels)) { - return null; - } - } - if (labels.isEmpty()) { + List labels = positiveLabelValuesOrNull(orStep); + if (labels == null) { return null; } @@ -294,6 +292,16 @@ private static OrStep positiveLabelOnlyOrStepAfter(Step step) { return (OrStep) next; } + private static List positiveLabelValuesOrNull(OrStep orStep) { + List labels = new ArrayList<>(); + for (Traversal.Admin child : orStep.getLocalChildren()) { + if (!collectPositiveLabelValues(child, labels)) { + return null; + } + } + return labels.isEmpty() ? null : labels; + } + private static boolean collectPositiveLabelValues( Traversal.Admin traversal, List labels) { if (traversal.getSteps().size() != 1) { @@ -602,6 +610,9 @@ private static boolean hasOnlyRangePredicates(HasContainer has) { public static void extractHasContainer(HugeVertexStep newStep, Traversal.Admin traversal) { + if (hasUnsafeLabelInTraversal(traversal, newStep)) { + return; + } Step step = newStep; do { Step nextStep = step.getNextStep(); @@ -645,7 +656,12 @@ private static boolean extractHasContainers(HugeVertexStep newStep, private static boolean canExtractHasContainers(HugeGraph graph, HasContainerHolder holder) { - for (HasContainer has : holder.getHasContainers()) { + // Keep unsafe labels and their sibling properties for local filtering. + if (hasUnsafeLabelPredicate(holder)) { + return false; + } + List hasContainers = holder.getHasContainers(); + for (HasContainer has : hasContainers) { if (!canExtractHasContainer(graph, has)) { return false; } @@ -653,6 +669,88 @@ private static boolean canExtractHasContainers(HugeGraph graph, return true; } + private static boolean hasUnsafeLabelInTraversal( + Traversal.Admin traversal, Step sourceStep) { + // Partial pushdown can lose candidates before local label filtering. + // Scan conservatively across the remaining traversal and its children; + // arbitrary extension steps don't reliably expose element identity. + // FIXME: Restore selective pushdown when every candidate schema label + // has compatible index coverage for extracted property predicates. + List steps = traversal.getSteps(); + int start = 0; + while (start < steps.size() && steps.get(start) != sourceStep) { + start++; + } + start++; + for (int i = start; i < steps.size(); i++) { + Step step = steps.get(i); + if (step instanceof HasStep) { + HasContainerHolder holder = (HasContainerHolder) step; + if (hasUnsafeLabelPredicate(holder)) { + return true; + } + } + if (hasUnsafeLabelInChildren(step)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelInChildren(Step step) { + if (!(step instanceof TraversalParent)) { + return false; + } + TraversalParent parent = (TraversalParent) step; + for (Traversal.Admin child : parent.getLocalChildren()) { + if (hasUnsafeLabelInChildTraversal(child)) { + return true; + } + } + for (Traversal.Admin child : parent.getGlobalChildren()) { + if (hasUnsafeLabelInChildTraversal(child)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelInChildTraversal( + Traversal.Admin traversal) { + for (Step childStep : traversal.getSteps()) { + if (childStep instanceof HasStep && + hasUnsafeLabelPredicate((HasContainerHolder) childStep)) { + return true; + } + if (hasUnsafeLabelInChildren(childStep)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelPredicate(HasContainerHolder holder) { + for (HasContainer has : holder.getHasContainers()) { + if (has.getKey().equals(T.label.getAccessor()) && + !isEqInLabelPredicate(has)) { + return true; + } + } + return false; + } + + private static boolean isEqInLabelPredicate(HasContainer has) { + 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..832730f52a 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,9 @@ 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.singleConditionValueOrNull( + 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 +460,7 @@ private boolean matchEdgeSortKeys(ConditionQuery query, boolean matchAll, HugeGraph graph) { assert query.resultType().isEdge(); - Id label = query.condition(HugeKeys.LABEL); + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); if (label == null) { return false; } 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 cbb2b7d043..df6bfeeb75 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 @@ -3559,6 +3559,177 @@ 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 testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossBarrier() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has(T.label, P.neq("reviewed")) + .barrier().has("score", 2).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + + edges = g.E().has("score", 2).barrier() + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossRange() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has("score", 2).skip(0) + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + + edges = g.E().has("score", 2).limit(1000) + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossMixedKeyOr() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has("score", 2) + .or(__.has(T.label, P.neq("reviewed")), + __.has("time", "2026-1-1")) + .toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByNonEqLabelAndIndexedPropertyAcrossSideEffect() { + HugeGraph graph = graph(); + initEdgeLabelQueryEdges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().has("score", 2).aggregate("x") + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + + edges = g.E().has("score", 2).coin(1.0D) + .has(T.label, P.neq("reviewed")).toList(); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals("recommended", edges.get(0).label()); + } + + @Test + public void testQueryEdgesByMixedConnectiveLabel() { + HugeGraph graph = graph(); + init18Edges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E() + .has(T.label, P.eq("created") + .or(P.neq("authored"))) + .toList(); + Assert.assertEquals(15, edges.size()); + for (Edge edge : edges) { + Assert.assertNotEquals("authored", edge.label()); + } + + edges = g.E() + .has(T.label, P.eq("created") + .and(P.neq("authored"))) + .toList(); + Assert.assertEquals(2, edges.size()); + for (Edge edge : edges) { + Assert.assertEquals("created", edge.label()); + } + } + + @Test + public void testQueryEdgesByMultipleNegativeLabelContainers() { + HugeGraph graph = graph(); + init18Edges(); + + GraphTraversalSource g = graph.traversal(); + + List edges = g.E().hasLabel("created") + .has(T.label, P.neq("authored")) + .toList(); + Assert.assertEquals(2, edges.size()); + for (Edge edge : edges) { + Assert.assertEquals("created", edge.label()); + } + + edges = g.E().has(T.label, P.neq("created")) + .has(T.label, P.neq("authored")) + .toList(); + Assert.assertEquals(13, edges.size()); + for (Edge edge : edges) { + Assert.assertNotEquals("created", edge.label()); + Assert.assertNotEquals("authored", edge.label()); + } + } + @Test public void testQueryOutEdgesOfVertexBySortkeyWithRange() { // FIXME: The legacy HStore guard and related coverage debt are tracked in @@ -7700,6 +7871,42 @@ 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(); + schema.indexLabel("reviewedByScore").onE("reviewed") + .secondary().by("score").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 9aa144542e..df4ba15dbf 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 @@ -41,6 +41,7 @@ import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.id.SnowflakeIdGenerator; import org.apache.hugegraph.backend.id.SplicingIdGenerator; +import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.query.Condition; import org.apache.hugegraph.backend.query.ConditionQuery; @@ -53,6 +54,7 @@ import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.schema.PropertyKey; +import org.apache.hugegraph.schema.SchemaLabel; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.schema.Userdata; import org.apache.hugegraph.schema.VertexLabel; @@ -9126,6 +9128,277 @@ public void testQueryByJointLabels() { Assert.assertEquals(0, vertices.size()); } + @Test + public void testQueryByNonEqLabelAndIndexedProperty() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has(T.label, P.neq("author")) + .has("city", "Beijing") + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByIndexedPropertyAndNonEqLabel() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing") + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossBarrier() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has(T.label, P.neq("author")) + .barrier().has("city", "Beijing") + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + + names = g.V().has("city", "Beijing").barrier() + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossRange() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing").skip(0) + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + + names = g.V().has("city", "Beijing").limit(1000) + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossMixedKeyOr() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing") + .or(__.has(T.label, P.neq("author")), + __.has("age", 20)) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @Test + public void testQueryByNonEqLabelAndIndexedPropertyAcrossSideEffect() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + + List names = g.V().has("city", "Beijing").aggregate("x") + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + + names = g.V().has("city", "Beijing").coin(1.0D) + .has(T.label, P.neq("author")) + .values("name").toList(); + Assert.assertEquals(4, names.size()); + Assert.assertEquals(ImmutableSet.of("James", "Tom Cat", "Lisa", + "unindexed-city-fan"), + ImmutableSet.copyOf(names)); + } + + @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 testQueryByMixedConnectiveLabel() { + HugeGraph graph = graph(); + init10Vertices(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V() + .has(T.label, P.eq("language") + .or(P.neq("author"))) + .toList(); + Assert.assertEquals(8, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertNotEquals("author", vertex.label()); + } + + vertices = g.V() + .has(T.label, P.eq("language") + .and(P.neq("author"))) + .toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("language", vertex.label()); + } + } + + @Test + public void testQueryByMultipleNegativeLabelContainers() { + HugeGraph graph = graph(); + init10Vertices(); + + GraphTraversalSource g = graph.traversal(); + + List vertices = g.V().hasLabel("language") + .has(T.label, P.neq("author")) + .toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("language", vertex.label()); + } + + vertices = g.V().has(T.label, P.neq("author")) + .has(T.label, P.neq("book")) + .toList(); + Assert.assertEquals(3, vertices.size()); + for (Vertex vertex : vertices) { + Assert.assertEquals("language", 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 testQueryByUserpropWithConflictingLabels() { + 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.eq(HugeKeys.LABEL, person.id()); + query.eq(HugeKeys.LABEL, computer.id()); + query.query(Condition.eq(city.id(), "Beijing")); + + IdHolderList holders = Whitebox.invoke(params().graphTransaction(), + "indexTx", + "queryByUserprop", + query); + Assert.assertTrue(holders.isEmpty()); + Assert.assertFalse(holders.paging()); + } + @Test public void testQueryByHasIdEmptyList() { HugeGraph graph = graph(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java index c79db5056f..bf2fd1b9d6 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/traversal/optimize/TraversalUtilOptimizeTest.java @@ -165,6 +165,71 @@ public void testExtractHasContainerExtractsPositiveLabelOnlyOrStep() { Assert.assertFalse(stepExists(traversal, OrStep.class)); } + @Test + public void testExtractHasContainerKeepsUnsafeLabelAfterPositiveLabelOr() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + Mockito.when(graph.vertexLabels()).thenReturn(Collections.emptyList()); + + Traversal.Admin traversal = traversal( + __.V().has("age", P.gt(1)) + .or(__.hasLabel("person"), __.hasLabel("software")) + .has(T.label, P.neq("author")) + .has("city", "Beijing"), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + Assert.assertTrue(hasStepExists(traversal, "city")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testExtractHasContainerKeepsMixedKeyOrUnsafeLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + PropertyKey status = propertyKey(3L, "status", DataType.TEXT); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + Mockito.when(graph.propertyKey("status")).thenReturn(status); + Mockito.when(graph.vertexLabels()).thenReturn(Collections.emptyList()); + + Traversal.Admin traversal = traversal( + __.V().has("city", "Beijing") + .or(__.has(T.label, P.neq("author")), + __.has("status", "active")), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "city")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testExtractHasContainerKeepsGlobalChildUnsafeLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + + Traversal.Admin traversal = traversal( + __.V().has("city", "Beijing") + .union(__.has(T.label, P.neq("author")), + __.identity()), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "city")); + } + @Test public void testExtractHasContainerKeepsUnsupportedOrLabelLocal() { Traversal.Admin traversal = __.V() @@ -263,6 +328,33 @@ public void testExtractHasContainerRemovesSafeGraphHasStep() { Assert.assertFalse(hasStepExists(traversal)); } + @Test + public void testExtractHasContainerKeepsGraphChainWithUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().has(T.label, P.neq("author")).barrier() + .has("age", 18), graph); + HugeGraphStep newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + + traversal = traversal( + __.V().has("age", 18).barrier() + .has(T.label, P.neq("author")), graph); + newStep = replaceGraphStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + } + @Test public void testExtractHasContainerKeepsTextRangeVertexHasStep() { HugeGraph graph = Mockito.mock(HugeGraph.class); @@ -296,6 +388,73 @@ public void testExtractHasContainerRemovesSafeVertexHasStep() { Assert.assertFalse(hasStepExists(traversal)); } + @Test + public void testExtractHasContainerKeepsVertexChainWithUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().out().has(T.label, P.neq("author")).barrier() + .has("age", 18), graph); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + + traversal = traversal( + __.V().out().has("age", 18).barrier() + .has(T.label, P.neq("author")), graph); + newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertEquals(2, countHasSteps(traversal)); + } + + @Test + public void testExtractHasContainerKeepsVertexMixedKeyOrUnsafeLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + PropertyKey city = propertyKey(2L, "city", DataType.TEXT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + Mockito.when(graph.propertyKey("city")).thenReturn(city); + + Traversal.Admin traversal = traversal( + __.V().out().has("age", 18) + .or(__.has(T.label, P.neq("author")), + __.has("city", "Beijing")), graph); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + + @Test + public void testExtractHasContainerKeepsTrailingUnsafeVertexLabelLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + PropertyKey age = propertyKey(1L, "age", DataType.INT); + Mockito.when(graph.propertyKey("age")).thenReturn(age); + + Traversal.Admin traversal = traversal( + __.V().out().has("age", P.gt(18)) + .or(__.hasLabel("person"), __.hasLabel("software")) + .has(T.label, P.neq("author")), graph); + HugeVertexStep newStep = replaceVertexStep(traversal); + + TraversalUtil.extractHasContainer(newStep, traversal); + + Assert.assertTrue(newStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "age")); + Assert.assertTrue(stepExists(traversal, OrStep.class)); + } + @Test public void testIsPositiveLabelContainer() { Assert.assertTrue(TraversalUtil.isPositiveLabelContainer( @@ -429,12 +588,17 @@ private static boolean hasContainer(HugeGraphStep step, String key) { } private static boolean hasStepExists(Traversal.Admin traversal) { + return countHasSteps(traversal) > 0; + } + + private static int countHasSteps(Traversal.Admin traversal) { + int count = 0; for (Step step : traversal.getSteps()) { if (step instanceof HasStep) { - return true; + count++; } } - return false; + return count; } private static boolean hasStepExists(Traversal.Admin traversal, 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 5778ceba7c..219adbfd00 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.singleConditionValueOrNull(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,53 @@ 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.singleConditionValueOrNull(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.singleConditionValueOrNull(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 testConditionWithEmptyInValues() { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of())); + + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableList.of(), + query.condition(HugeKeys.LABEL)); + } + @Test public void testConditionWithConflictingEqAndIn() { Id label1 = IdGenerator.of(1); @@ -73,6 +130,29 @@ 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.singleConditionValueOrNull(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.assertEquals(ImmutableSet.of(), + query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); Assert.assertNull(query.condition(HugeKeys.LABEL)); } @@ -89,6 +169,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.singleConditionValueOrNull(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'",