diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 13f363741a..acd9c45186 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -195,7 +195,7 @@ jobs: uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ${{ env.REPORT_DIR }}/*.xml + files: ${{ env.REPORT_DIR }}/*.xml,hugegraph-server/target/site/jacoco/jacoco.xml disable_search: true fail_ci_if_error: false diff --git a/docs/shared-foundation-migration.md b/docs/shared-foundation-migration.md index f39dc54673..b7221f5b9b 100644 --- a/docs/shared-foundation-migration.md +++ b/docs/shared-foundation-migration.md @@ -86,6 +86,33 @@ Keep the core imports for `SnowflakeIdGenerator` and the query execution helpers `QueryResults`, `ConditionQueryFlatten`, `EdgesQueryIterator`, `QueryBatch` and `QueryResultContext`. The table applies to shared types, not entire packages. `SchemaManager`, schema mutation builders and backend-specific serializers remain in core. There is no general compatibility package for removed core classes. Relocated types also change method descriptors that expose IDs, schema, queries and indexes, so update implementations and call sites and recompile every affected integration against matching artifacts. Changing source imports does not make old binaries compatible. +### Resolve conditions through the shared query type + +Server and Store now use `org.apache.hugegraph.query.ConditionQuery` from struct. +The explicit resolution methods operate on top-level `EQ` and `IN` relations +for a key and intersect their candidate values. Nested conditions and other +operators remain outside this resolution. + +| Method | Result | +|--------|--------| +| `containsCondition(HugeKeys)` | Reports whether any top-level relation exists for the system key. | +| `containsConditionValues(key)` | Reports whether a top-level `EQ` or `IN` relation exists, including an empty `IN`. | +| `conditionValues(key)` | Returns the candidate intersection. Pair it with `containsConditionValues` to distinguish absence from an empty intersection. | +| `conditionValue(key)` | Returns `null` for an empty intersection, the value for a singleton, and throws for multiple candidates. | +| `singleConditionValueOrNull(key)` | Returns the value for a singleton and `null` for every other candidate count. | + +The existing `condition(key)` method keeps its legacy behavior, including +returning the original list for a sole `IN` relation. Use strict +`conditionValue` when a serializer requires one value; use +`singleConditionValueOrNull` when an optimization applies only to one candidate. +For example, `IN [1, 2]` combined with `EQ 2` resolves to `2`, while an +additional `EQ 3` produces an empty intersection. + +SEARCH index construction and local SEARCH matching share the analyzer and +term handling in struct's `IndexBuilder`. Engine callers use +`HugeGraph.searchPredicate(text)` so the graph selects its configured analyzer +and the authorization proxy checks access before delegation. + ### Check API and SPI implementations | Integration point | Required change or preserved contract | diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java index 2ae3d7312e..f4066cf154 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/auth/HugeGraphAuthProxy.java @@ -35,6 +35,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; import java.util.function.Supplier; import javax.security.sasl.AuthenticationException; @@ -764,6 +765,12 @@ public V option(TypedOption option) { return this.hugegraph.option(option); } + @Override + public Predicate searchPredicate(String text) { + this.verifyAnyPermission(); + return this.hugegraph.searchPredicate(text); + } + @Override public String name() { this.verifyAnyPermission(); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java index 6cc903a64d..56bc00c0bb 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/HugeGraph.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import org.apache.commons.lang3.tuple.Pair; import org.apache.hugegraph.auth.AuthManager; @@ -286,6 +287,11 @@ public interface HugeGraph extends Graph, HugeGraphSupplier { V option(TypedOption option); + // Build the same term matcher used by SEARCH indexes, without reading data. + default Predicate searchPredicate(String text) { + throw new UnsupportedOperationException("Local SEARCH matching is not supported by this graph"); + } + void registerRpcServices(RpcServiceConfig4Server serverConfig, RpcServiceConfig4Client clientConfig); diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java index 3191fa91e9..4649dd1fc9 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/StandardHugeGraph.java @@ -29,6 +29,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.analyzer.Analyzer; @@ -36,21 +37,16 @@ import org.apache.hugegraph.auth.AuthManager; import org.apache.hugegraph.auth.StandardAuthManager; import org.apache.hugegraph.auth.StandardAuthManagerV2; -import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.backend.LocalCounter; import org.apache.hugegraph.backend.cache.Cache; -import org.apache.hugegraph.backend.cache.CacheNotifier; import org.apache.hugegraph.backend.cache.CacheNotifier.GraphCacheNotifier; import org.apache.hugegraph.backend.cache.CacheNotifier.SchemaCacheNotifier; +import org.apache.hugegraph.backend.cache.CacheNotifier; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; import org.apache.hugegraph.backend.cache.CachedSchemaTransaction; import org.apache.hugegraph.backend.cache.CachedSchemaTransactionV2; -import org.apache.hugegraph.id.Id; -import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.id.SnowflakeIdGenerator; -import org.apache.hugegraph.query.Query; import org.apache.hugegraph.backend.serializer.AbstractSerializer; -import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.serializer.SerializerFactory; import org.apache.hugegraph.backend.store.BackendFeatures; import org.apache.hugegraph.backend.store.BackendProviderFactory; @@ -67,7 +63,10 @@ import org.apache.hugegraph.config.TypedOption; import org.apache.hugegraph.event.EventHub; import org.apache.hugegraph.event.EventListener; +import org.apache.hugegraph.exception.BackendException; import org.apache.hugegraph.exception.NotAllowException; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.io.HugeGraphIoRegistry; import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.kvstore.KvStore; @@ -76,20 +75,23 @@ import org.apache.hugegraph.memory.util.RoundUtil; import org.apache.hugegraph.meta.MetaManager; import org.apache.hugegraph.perf.PerfUtil.Watched; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.rpc.RpcServiceConfig4Client; import org.apache.hugegraph.rpc.RpcServiceConfig4Server; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.struct.schema.SchemaElement; import org.apache.hugegraph.struct.schema.SchemaLabel; -import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeEdgeProperty; import org.apache.hugegraph.structure.HugeFeatures; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.structure.HugeVertexProperty; +import org.apache.hugegraph.structure.builder.IndexBuilder; import org.apache.hugegraph.task.EphemeralJobQueue; import org.apache.hugegraph.task.ServerInfoManager; import org.apache.hugegraph.task.TaskManager; @@ -731,6 +733,11 @@ private Analyzer analyzer() { return AnalyzerFactory.analyzer(name, mode); } + @Override + public Predicate searchPredicate(String text) { + return IndexBuilder.searchPredicate(this.analyzer(), text); + } + protected void reloadRamtable() { this.reloadRamtable(false); } 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 ce79e5b5aa..cd89f36dbc 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 445d028cf7..04cf773664 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 @@ -563,7 +563,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); @@ -611,7 +611,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 5a7ca40f7b..0d66a76446 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 707f02c9be..c9d5edfce1 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 @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -53,6 +54,7 @@ import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.util.Consumers; +import org.apache.hugegraph.util.E; import org.apache.hugegraph.util.Log; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Vertex; @@ -269,7 +271,18 @@ 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); + // query() expands one nonempty LABEL IN relation into EQ branches. + // Do not mistake an empty/conflicting set for the wildcard label zero. + Set labels = cq.conditionValues(HugeKeys.LABEL); + for (Object value : labels) { + if (!(value instanceof Id)) { + return false; + } + Id label = (Id) value; + if (!label.number() || label.asLong() <= 0L || label.asLong() > Integer.MAX_VALUE) { + return false; + } + } if (direction == null && conditionsSize > 1) { for (Condition cond : cq.conditions()) { @@ -289,7 +302,7 @@ public boolean matched(Query query) { if (direction != null) { matchedConds++; } - if (label != null) { + if (!labels.isEmpty()) { matchedConds++; } return matchedConds == cq.conditionsSize(); @@ -316,9 +329,12 @@ private Iterator query(ConditionQuery query) { if (dir == null) { dir = Directions.BOTH; } - Id label = query.condition(HugeKeys.LABEL); - if (label == null) { - label = IdGenerator.ZERO; + Id label = IdGenerator.ZERO; + if (query.containsConditionValues(HugeKeys.LABEL)) { + // Only an absent EQ/IN label condition means all labels. Each + // flattened branch must otherwise resolve to exactly one label. + label = query.conditionValue(HugeKeys.LABEL); + E.checkState(label != null, "Expect one label value for query: %s", query); } return this.query(owner.asLong(), dir, (int) label.asLong()); } 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 5b5289dd59..8dc0b54d39 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 @@ -20,6 +20,7 @@ import java.nio.CharBuffer; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -30,53 +31,53 @@ import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.HugeGraphParams; -import org.apache.hugegraph.id.Id; -import org.apache.hugegraph.backend.page.IdHolder; import org.apache.hugegraph.backend.page.IdHolder.BatchIdHolder; import org.apache.hugegraph.backend.page.IdHolder.FixedIdHolder; import org.apache.hugegraph.backend.page.IdHolder.PagingIdHolder; +import org.apache.hugegraph.backend.page.IdHolder; import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageIds; import org.apache.hugegraph.backend.page.PageInfo; import org.apache.hugegraph.backend.page.PageState; import org.apache.hugegraph.backend.page.SortByCountIdHolderList; -import org.apache.hugegraph.query.Condition; -import org.apache.hugegraph.query.Condition.RangeConditions; -import org.apache.hugegraph.query.Condition.Relation; -import org.apache.hugegraph.query.Condition.RelationType; -import org.apache.hugegraph.query.ConditionQuery; -import org.apache.hugegraph.query.ConditionQuery.OptimizedType; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; -import org.apache.hugegraph.query.Query; -import org.apache.hugegraph.query.MatchedIndex; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.backend.store.BackendStore; import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; import org.apache.hugegraph.exception.NotSupportException; +import org.apache.hugegraph.id.Id; import org.apache.hugegraph.iterator.Metadatable; import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.job.system.DeleteExpiredJob; import org.apache.hugegraph.perf.PerfUtil.Watched; +import org.apache.hugegraph.query.Condition.RangeConditions; +import org.apache.hugegraph.query.Condition.Relation; +import org.apache.hugegraph.query.Condition.RelationType; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery.OptimizedType; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.MatchedIndex; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeElement; -import org.apache.hugegraph.structure.Index; -import org.apache.hugegraph.structure.builder.IndexBuilder; -import org.apache.hugegraph.structure.Index.IdWithExpiredTime; import org.apache.hugegraph.structure.HugeProperty; -import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.structure.Index.IdWithExpiredTime; +import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.structure.builder.IndexBuilder; import org.apache.hugegraph.task.EphemeralJobQueue; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Action; @@ -282,10 +283,11 @@ 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 - return this.queryByLabel(query); + label != null) { + // Query only by one EQ/IN-resolved label + return this.queryByLabel(query, label); } else { // Query by userprops (or userprops + label) return this.queryByUserprop(query); @@ -293,13 +295,10 @@ public IdHolderList queryIndex(ConditionQuery query) { } @Watched(prefix = "index") - private IdHolderList queryByLabel(ConditionQuery query) { + private IdHolderList queryByLabel(ConditionQuery query, Id label) { HugeType queryType = query.resultType(); IndexLabel il = IndexLabel.label(queryType); validateIndexLabel(il); - Id label = query.condition(HugeKeys.LABEL); - assert label != null; - HugeType indexType; SchemaLabel schemaLabel; if (queryType.isVertex()) { @@ -362,14 +361,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); } @@ -650,11 +653,16 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, @Watched(prefix = "index") private Set collectMatchedIndexes(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); - Id label = query.condition(HugeKeys.LABEL); + Set labels = query.conditionValues(HugeKeys.LABEL); List schemaLabels; - if (label != null) { - // Query has LABEL condition + if (query.containsConditionValues(HugeKeys.LABEL) && labels.isEmpty()) { + // An empty intersection is not an unrestricted label query. + 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); @@ -667,7 +675,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()) { @@ -783,9 +792,7 @@ private ConditionQuery constructSearchQuery(ConditionQuery query, MatchedIndex i } private boolean matchSearchIndexWords(String propValue, String fieldValue) { - Set propValues = this.segmentWords(propValue); - Set words = this.segmentWords(fieldValue); - return CollectionUtil.hasIntersection(propValues, words); + return IndexBuilder.searchPredicate(this.params().analyzer(), fieldValue).test(propValue); } private Set segmentWords(String text) { @@ -1593,7 +1600,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)) { @@ -1628,7 +1635,10 @@ private long processRangeIndexLeft(ConditionQuery query, Set indexValues = leftIndex.indexFieldValues(); IndexLabel indexLabel = this.findMatchedIndexLabel(query, leftIndex); - assert indexLabel != null; + if (indexLabel == null) { + // No matching schema index remains for this stale entry. + continue; + } AbstractSerializer serializer = this.tx.serializer; for (Object value : indexValues) { 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 e1b5c37d27..b0093b924c 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 @@ -1093,7 +1093,7 @@ private QueryResults queryEdgeBatchesFromBackend(Query query) { List flattened = ConditionQueryFlatten.flatten( (ConditionQuery) query, supportIn); Function> fetcher = cq -> { - Id label = cq.condition(HugeKeys.LABEL); + Id label = cq.singleConditionValueOrNull(HugeKeys.LABEL); if (this.storeFeatures().supportsFatherAndSubEdgeLabel() && label != null && graph().edgeLabel(label).isFather() && cq.condition(HugeKeys.SUB_LABEL) == null && @@ -1414,7 +1414,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; } @@ -1547,7 +1547,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()) { @@ -1937,7 +1937,8 @@ private boolean rightResultFromIndexQuery(QueryResultContext context, HugeElemen if (cq == null || cq.resultType().isVertex() != elem.type().isVertex()) { return true; } - 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 a7bbdb9723..4989c6ecc6 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 d85015c97c..f51d2b7b0e 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 @@ -19,31 +19,36 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; +import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.backend.page.PageInfo; +import org.apache.hugegraph.backend.page.PageState; import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.exception.NotFoundException; +import org.apache.hugegraph.exception.NotSupportException; import org.apache.hugegraph.id.EdgeId; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; -import org.apache.hugegraph.backend.page.PageInfo; -import org.apache.hugegraph.backend.page.PageState; +import org.apache.hugegraph.iterator.FilterIterator; import org.apache.hugegraph.query.Aggregate; import org.apache.hugegraph.query.Condition; import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.query.Query; -import org.apache.hugegraph.exception.NotFoundException; -import org.apache.hugegraph.exception.NotSupportException; -import org.apache.hugegraph.iterator.FilterIterator; import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.struct.schema.SchemaLabel; @@ -67,26 +72,71 @@ import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder; import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.AndStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.ClassFilterStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.CoinStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.DedupGlobalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.FilterStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.IsStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NoneStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.NotStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.OrStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.PathFilterStep; import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.SampleGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TailGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TimeLimitStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilterStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ConstantStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.DedupLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ElementMapStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.FoldStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GroupCountStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GroupStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.IdStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.IndexStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MatchStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.OrderLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.ProjectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertiesStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertyKeyStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertyMapStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.PropertyValueStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.RangeLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.SampleLocalStep; import org.apache.tinkerpop.gremlin.process.traversal.step.map.SumGlobalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.SumLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.TailLocalStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.UnfoldStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.AggregateStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.GroupCountSideEffectStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.GroupSideEffectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.IdentityStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.ProfileSideEffectStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.SubgraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.sideEffect.TraversalSideEffectStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.ElementValueComparator; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.EmptyStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.ProfileStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.ReducingBarrierStep; import org.apache.tinkerpop.gremlin.process.traversal.util.AndP; import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP; @@ -102,6 +152,7 @@ import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; public final class TraversalUtil { @@ -110,6 +161,34 @@ public final class TraversalUtil { public static final String P_CALL = "P."; + // Standard filters, current-value projections/reductions (including local + // collection variants), and side effects that return the current traverser. + // Exact classes keep extension subclasses conservative. History/state readers + // (select/path/tree, sack/cap, labeled where/match), repeat/branches, mutations, + // lambdas, custom folds and unknown steps are deliberately not admitted. + private static final Set> CURRENT_ELEMENT_SUFFIX_STEPS = ImmutableSet.of( + VertexStep.class, HugeVertexStep.class, HugeVertexStepByBatch.class, + PropertiesStep.class, HasStep.class, LocalContainsStep.class, + AndStep.class, OrStep.class, NotStep.class, TraversalFilterStep.class, + IsStep.class, ClassFilterStep.class, NoneStep.class, CoinStep.class, + RangeGlobalStep.class, TailGlobalStep.class, SampleGlobalStep.class, + DedupGlobalStep.class, PathFilterStep.class, TimeLimitStep.class, + NoOpBarrierStep.class, IdentityStep.class, + IdStep.class, LabelStep.class, PropertyKeyStep.class, PropertyValueStep.class, + PropertyMapStep.class, ElementMapStep.class, ProjectStep.class, ConstantStep.class, + FoldStep.class, UnfoldStep.class, IndexStep.class, + GroupStep.class, GroupCountStep.class, + OrderGlobalStep.class, OrderLocalStep.class, RangeLocalStep.class, TailLocalStep.class, + DedupLocalStep.class, SampleLocalStep.class, + CountGlobalStep.class, SumGlobalStep.class, MinGlobalStep.class, + MaxGlobalStep.class, MeanGlobalStep.class, + CountLocalStep.class, SumLocalStep.class, MinLocalStep.class, + MaxLocalStep.class, MeanLocalStep.class, + AggregateStep.class, + GroupSideEffectStep.class, GroupCountSideEffectStep.class, + TraversalSideEffectStep.class, + SubgraphStep.class, ProfileStep.class, ProfileSideEffectStep.class); + public static HugeGraph getGraph(Step step) { HugeGraph graph = tryGetGraph(step); if (graph != null) { @@ -179,6 +258,10 @@ public static void trySetGraph(Step step, HugeGraph graph) { public static void extractHasContainer(HugeGraphStep newStep, Traversal.Admin traversal) { + if (hasUnsafeLabelInTraversal(traversal, newStep)) { + prepareLocalHasContainers(newStep, traversal); + return; + } Step step = newStep.getNextStep(); while (step instanceof HasStep || step instanceof NoOpBarrierStep) { Step nextStep = step.getNextStep(); @@ -267,13 +350,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; } @@ -301,6 +379,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) { @@ -632,6 +720,10 @@ private static boolean hasOnlyRangePredicates(HasContainer has) { public static void extractHasContainer(HugeVertexStep newStep, Traversal.Admin traversal) { + if (hasUnsafeLabelInTraversal(traversal, newStep)) { + prepareLocalHasContainers(newStep, traversal); + return; + } Step step = newStep; do { Step nextStep = step.getNextStep(); @@ -676,7 +768,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; } @@ -684,6 +781,517 @@ private static boolean canExtractHasContainers(HugeGraph graph, return true; } + private static void prepareLocalHasContainers( + Step source, Traversal.Admin traversal) { + QueryHolder query = (QueryHolder) source; + Step step = source.getNextStep(); + while (step instanceof HasStep || step instanceof NoOpBarrierStep) { + Step next = step.getNextStep(); + if (step instanceof HasStep) { + HasContainerHolder holder = (HasContainerHolder) step; + for (HasContainer has : new ArrayList<>(holder.getHasContainers())) { + // Paging is query metadata, never an element property filter. + if (QueryHolder.SYSPROP_PAGE.equals(has.getKey())) { + query.addHasContainer(has); + holder.removeHasContainer(has); + continue; + } + if (T.id.getAccessor().equals(has.getKey())) { + // ID lookup is complete across labels. Existing source + // IDs and unsupported predicates still filter locally. + if (source instanceof HugeGraphStep && + GraphStep.processHasContainerIds((HugeGraphStep) source, has)) { + holder.removeHasContainer(has); + continue; + } + holder.removeHasContainer(has); + holder.addHasContainer(new LocalIdHasContainer(localIdPredicate(has.getPredicate()))); + continue; + } + if (T.label.getAccessor().equals(has.getKey())) { + holder.removeHasContainer(has); + HasContainer label = new LocalLabelHasContainer(has.getPredicate()); + if (source instanceof HugeGraphStep && + canPushPositiveLabel((HugeGraphStep) source, has)) { + // A positive label conjunct uses the label index, + // independent of per-label property index coverage. + // Keep the runtime label matcher for source-ID queries. + query.addHasContainer(label); + } else { + // Keep unsupported candidates and adjacent-vertex + // labels local, including their paging boundary. + holder.addHasContainer(label); + } + continue; + } + if (keyForContainsKey(has.getKey()) || keyForContainsValue(has.getKey())) { + // Backend CONTAINS support varies and source IDs use + // local filtering too. Preserve HugeGraph's map query + // semantics without treating "key"/"value" as names. + holder.removeHasContainer(has); + traversal.addStep(traversal.getSteps().indexOf(step), + new LocalContainsStep<>(traversal, has)); + continue; + } + if (containsSearchPredicate(has.getPredicate())) { + // Child traversals can be unbound during optimization; + // the matcher resolves the graph from each runtime element. + holder.removeHasContainer(has); + holder.addHasContainer(new LocalSearchHasContainer( + has.getKey(), has.getPredicate())); + } + } + if (holder.getHasContainers().isEmpty()) { + TraversalHelper.copyLabels(step, step.getPreviousStep(), false); + traversal.removeStep(step); + } + } + step = next; + } + if (query.queryInfo().paging() && step instanceof RangeGlobalStep) { + // Bound the raw backend page even when local filters prevent normal + // range extraction. Keep the range step to apply offset/limit after + // those filters; a filtered page may contain fewer results. + query.setRange(0, ((RangeGlobalStep) step).getHighRange()); + } + } + + private static boolean canPushPositiveLabel(HugeGraphStep source, + HasContainer has) { + if (!isEqInLabelPredicate(has)) { + return false; + } + HugeGraph graph = tryGetGraph(source); + if (graph == null) { + return false; + } + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P predicate : predicates) { + Object value = predicate.getValue(); + BiPredicate bp = predicate.getBiPredicate(); + if (bp == Contains.within && !(value instanceof Collection)) { + return false; + } + Collection values = bp == Contains.within ? + (Collection) value : Collections.singletonList(value); + for (Object candidate : values) { + if (!hasLabelIndex(graph, source.returnsVertex(), candidate)) { + return false; + } + } + } + return true; + } + + private static boolean hasLabelIndex(HugeGraph graph, boolean vertex, Object value) { + if (value instanceof Number) { + value = IdGenerator.of(((Number) value).longValue()); + } + try { + SchemaLabel label; + if (value instanceof Id) { + Id id = (Id) value; + // Nonpositive IDs include internal schema objects that aren't + // user labels. Preserve their local matching behavior. + if (!id.number() || id.asLong() <= 0L) { + return false; + } + label = vertex ? graph.vertexLabel(id) : graph.edgeLabel(id); + } else if (value instanceof String) { + label = vertex ? graph.vertexLabel((String) value) : graph.edgeLabel((String) value); + } else { + return false; + } + // Do not turn a working local filter into a missing-label/index error. + return label != null && label.enableLabelIndex(); + } catch (IllegalArgumentException | NotFoundException e) { + return false; + } + } + + private static P localIdPredicate(P predicate) { + // Keep IDs local to preserve source-ID intersections and step ordering. + // UUID/Element values need the same representation as HugeElement.id(). Leave + // strings unchanged so HasContainer retains its string-ID comparison. + P copy = copyResolvedPredicate(predicate); + List> leaves = new ArrayList<>(); + collectPredicates(leaves, ImmutableList.of(copy)); + for (P leaf : leaves) { + Object value = leaf.getValue(); + if (value instanceof Collection) { + List values = new ArrayList<>(); + for (Object item : (Collection) value) { + values.add(localIdValue(item)); + } + leaf.setValue(values); + } else { + leaf.setValue(localIdValue(value)); + } + } + return copy; + } + + private static Object localIdValue(Object value) { + if (value instanceof UUID || value instanceof Element) { + return HugeElement.getIdValue(HugeType.VERTEX, value); + } + return value; + } + + private static final class LocalLabelHasContainer extends HasContainer { + + private static final long serialVersionUID = 1L; + + private LocalLabelHasContainer(P predicate) { + super(T.label.getAccessor(), copyResolvedPredicate(predicate)); + } + + @Override + protected boolean testLabel(Element element) { + // Resolve against the actual element, not a graph captured while + // strategies run. Child traversals may be unbound or later cloned. + return testLabelPredicate(this.getPredicate(), ((HugeElement) element).schemaLabel()); + } + } + + private static final class LocalContainsStep extends HasStep { + + private static final long serialVersionUID = 1L; + + private LocalContainsStep(Traversal.Admin traversal, HasContainer has) { + super(traversal, has.clone()); + E.checkArgument(has.getPredicate().getBiPredicate() == Compare.eq, + "CONTAINS query with relation '%s' is not supported", + has.getPredicate().getBiPredicate()); + } + + @Override + protected boolean filter(Traverser.Admin traverser) { + HugeElement element = (HugeElement) traverser.get(); + // Adjacent vertices can be ID/label-only shells. Like properties(), + // load their properties before evaluating the system property map. + element.getFilledProperties(); + // Keep a HasStep boundary so count/range cannot bypass this filter. + // Resolve schema from the runtime element; clone/reset must not + // retain a graph or transaction captured during optimization. + for (HasContainer has : this.getHasContainers()) { + boolean matches = keyForContainsKey(has.getKey()) || keyForContainsValue(has.getKey()) ? + convContains2Relation(element.graph(), has).test(element.element()) : + has.test(element); + if (!matches) { + return false; + } + } + return true; + } + } + + @SuppressWarnings("unchecked") + private static boolean testLabelPredicate(P predicate, SchemaLabel label) { + if (predicate instanceof NotP) { + return !testLabelPredicate(((NotP) predicate).negate(), label); + } + if (predicate instanceof ConnectiveP) { + boolean and = predicate instanceof AndP; + for (P child : ((ConnectiveP) predicate).getPredicates()) { + if (testLabelPredicate(child, label) != and) { + return !and; + } + } + return and; + } + BiPredicate bp = predicate.getBiPredicate(); + Object value = predicate.getValue(); + if (bp == Contains.within || bp == Contains.without) { + for (Object item : (Collection) value) { + if (testLabelValue(Compare.eq, label, item)) { + return bp == Contains.within; + } + } + return bp == Contains.without; + } + return testLabelValue((BiPredicate) bp, label, value); + } + + private static boolean testLabelValue(BiPredicate predicate, + SchemaLabel label, Object value) { + if (value instanceof Number) { + value = IdGenerator.of(((Number) value).longValue()); + } + return predicate.test(value instanceof Id ? label.id() : label.name(), value); + } + + private static final class LocalSearchHasContainer extends HasContainer { + + private static final long serialVersionUID = 1L; + + private transient HugeGraph matcherGraph; + private transient P matcherPredicate; + private transient Predicate matcher; + + private LocalSearchHasContainer(String key, P predicate) { + super(key, copyResolvedPredicate(predicate)); + } + + @Override + protected boolean testValue(Property property) { + // Keep the public P tree intact for strategies, hashing and Java + // serialization. Resolve the analyzer from the element's graph, + // including after cloning, deserialization or graph rebinding. + HugeGraph graph = (HugeGraph) property.element().graph(); + if (this.matcherGraph != graph || + !samePredicateValues(this.getPredicate(), this.matcherPredicate)) { + P predicate = copyResolvedPredicate(this.getPredicate()); + this.matcher = localSearchMatcher(predicate, graph); + this.matcherPredicate = predicate; + this.matcherGraph = graph; + } + return this.matcher.test(property.value()); + } + + @Override + public LocalSearchHasContainer clone() { + LocalSearchHasContainer clone = (LocalSearchHasContainer) super.clone(); + clone.matcherGraph = null; + clone.matcherPredicate = null; + clone.matcher = null; + return clone; + } + } + + private static boolean samePredicateValues(P current, P cached) { + // P.equals() compares originalValue, not the value changed by setValue(). + if (cached == null || current.getClass() != cached.getClass()) { + return false; + } + if (current instanceof NotP) { + return samePredicateValues(((NotP) current).negate(), ((NotP) cached).negate()); + } + if (current instanceof ConnectiveP) { + List> children = ((ConnectiveP) current).getPredicates(); + List> oldChildren = ((ConnectiveP) cached).getPredicates(); + if (children.size() != oldChildren.size()) { + return false; + } + for (int i = 0; i < children.size(); i++) { + if (!samePredicateValues(children.get(i), oldChildren.get(i))) { + return false; + } + } + return true; + } + return current.getBiPredicate().equals(cached.getBiPredicate()) && + Objects.equals(current.getValue(), cached.getValue()); + } + + private static boolean containsSearchPredicate(P predicate) { + if (predicate instanceof NotP) { + return containsSearchPredicate(((NotP) predicate).negate()); + } + if (predicate instanceof ConnectiveP) { + for (P child : ((ConnectiveP) predicate).getPredicates()) { + if (containsSearchPredicate(child)) { + return true; + } + } + return false; + } + return predicate.getBiPredicate() == Condition.RelationType.TEXT_CONTAINS; + } + + @SuppressWarnings("unchecked") + private static Predicate localSearchMatcher(P predicate, HugeGraph graph) { + if (predicate instanceof NotP) { + Predicate child = localSearchMatcher(((NotP) predicate).negate(), graph); + return value -> !child.test(value); + } + if (predicate instanceof ConnectiveP) { + List> children = new ArrayList<>(); + for (P child : ((ConnectiveP) predicate).getPredicates()) { + children.add(localSearchMatcher(child, graph)); + } + boolean and = predicate instanceof AndP; + return value -> { + for (Predicate child : children) { + if (child.test(value) != and) { + return !and; + } + } + return and; + }; + } + if (predicate.getBiPredicate() != Condition.RelationType.TEXT_CONTAINS) { + return (P) predicate; + } + // Match SEARCH terms in place, preserving range and side-effect ordering. + return graph.searchPredicate((String) predicate.getValue()); + } + + private static boolean hasUnsafeLabelInTraversal( + Traversal.Admin traversal, Step sourceStep) { + // Partial pushdown can lose candidates before local label filtering. + // Scan the remaining traversal, its children and each ancestor's + // remaining steps for filters on child output. Arbitrary extension + // steps don't reliably expose element identity, so stay conservative. + // FIXME(#3201): Restore selective pushdown when every candidate schema label + // has compatible index coverage for extracted property predicates. + // Outside proven root and child suffixes, negative labels can disable + // property pushdown even across element changes (including ancestors + // and unknown extension steps), potentially requiring a full scan. + 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 (changesCurrentElement(step) && + traversal.getParent() instanceof EmptyStep && + onlyCurrentElementSuffix(steps.subList(i, steps.size()))) { + return false; + } + if (step instanceof HasStep) { + HasContainerHolder holder = (HasContainerHolder) step; + if (hasUnsafeLabelPredicate(holder)) { + return true; + } + } + if (hasUnsafeLabelInChildren(step)) { + return true; + } + } + TraversalParent parent = traversal.getParent(); + if (parent instanceof Step && !(parent instanceof EmptyStep)) { + Step parentStep = (Step) parent; + // RepeatStep's until/emit siblings can filter this child's output. + // This helper only descends, so revisiting the owning step's children + // cannot recurse back into this ancestor walk. + if (hasUnsafeLabelInChildren(parentStep)) { + return true; + } + return hasUnsafeLabelInTraversal(parentStep.getTraversal(), parentStep); + } + return false; + } + + private static boolean changesCurrentElement(Step step) { + // Vertex adjacency is a scope boundary for this label fallback, not + // proof that a self-loop or later hop cannot return the source. + // Preserve the source property query's existing index/error semantics; + // complete cross-label property candidates are tracked in #3201. + // Edge endpoints can recover the source (outE().outV(), inE().inV(), + // bothV()). Do not admit EdgeVertexStep here or in the suffix allowlist: + // checking only that step is too late if outE() already ended the scan. + return step instanceof VertexStep || step instanceof PropertiesStep; + } + + private static boolean onlyCurrentElementSuffix(List steps) { + for (Step step : steps) { + // Check both child traversals and hidden history/lambda inputs. + // dedup("a") reads step labels even without a select() child; + // fold(seed, function) can run arbitrary code unlike list fold(). + if (!CURRENT_ELEMENT_SUFFIX_STEPS.contains(step.getClass()) || + step instanceof DedupGlobalStep && + !((DedupGlobalStep) step).getScopeKeys().isEmpty() || + step instanceof FoldStep && !((FoldStep) step).isListFold()) { + return false; + } + if (step instanceof TraversalParent) { + TraversalParent parent = (TraversalParent) step; + for (Traversal.Admin child : parent.getLocalChildren()) { + if (!onlyCurrentElementSuffix(child.getSteps())) { + return false; + } + } + for (Traversal.Admin child : parent.getGlobalChildren()) { + if (!onlyCurrentElementSuffix(child.getSteps())) { + return false; + } + } + } + } + return true; + } + + private static boolean hasUnsafeLabelInChildren(Step step) { + return hasUnsafeLabelInChildren(step, false); + } + + private static boolean hasUnsafeLabelInChildren(Step step, boolean negated) { + if (!(step instanceof TraversalParent)) { + return false; + } + TraversalParent parent = (TraversalParent) step; + // Even an EQ label under not() describes a complement, whose property + // index coverage is unknown. Stay conservative for nested negations too. + negated |= step instanceof NotStep; + for (Traversal.Admin child : parent.getLocalChildren()) { + if (hasUnsafeLabelInChildTraversal(child, negated)) { + return true; + } + } + for (Traversal.Admin child : parent.getGlobalChildren()) { + if (hasUnsafeLabelInChildTraversal(child, negated)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelInChildTraversal( + Traversal.Admin traversal, boolean negated) { + List steps = traversal.getSteps(); + for (int i = 0; i < steps.size(); i++) { + Step childStep = steps.get(i); + // A child may filter a different element without constraining the + // source label. Only stop when the suffix cannot recover the source + // through select/path, repeat, or an unknown extension step. + if (changesCurrentElement(childStep) && + onlyCurrentElementSuffix(steps.subList(i, steps.size()))) { + return false; + } + if (childStep instanceof HasStep && + hasUnsafeLabelPredicate((HasContainerHolder) childStep, negated)) { + return true; + } + if (hasUnsafeLabelInChildren(childStep, negated)) { + return true; + } + } + return false; + } + + private static boolean hasUnsafeLabelPredicate(HasContainerHolder holder) { + return hasUnsafeLabelPredicate(holder, false); + } + + private static boolean hasUnsafeLabelPredicate(HasContainerHolder holder, boolean negated) { + for (HasContainer has : holder.getHasContainers()) { + if (T.label.getAccessor().equals(has.getKey()) && + (negated || !isEqInLabelPredicate(has))) { + return true; + } + } + return false; + } + + private static boolean isEqInLabelPredicate(HasContainer has) { + if (has.getPredicate() == null) { + return false; + } + List> predicates = new ArrayList<>(); + collectPredicates(predicates, ImmutableList.of(has.getPredicate())); + for (P predicate : predicates) { + BiPredicate bp = predicate.getBiPredicate(); + if (bp != Compare.eq && bp != Contains.within) { + return false; + } + } + return true; + } + static boolean canExtractHasContainer(HugeGraph graph, HasContainer has) { if (has.getKey() == null || has.getPredicate() == null || @@ -1182,7 +1790,18 @@ public static Iterator filterResult( return (Iterator) iterator; } Iterator result = new FilterIterator<>(iterator, elem -> { - return HasContainer.testAll(elem, hasContainers); + for (HasContainer has : hasContainers) { + // Explicit-ID queries bypass backend label conversion. Keep the + // same name/Id/number semantics as backend and fallback queries. + boolean matches = T.label.getAccessor().equals(has.getKey()) && + elem instanceof HugeElement ? + testLabelPredicate(has.getPredicate(), + ((HugeElement) elem).schemaLabel()) : has.test(elem); + if (!matches) { + return false; + } + } + return true; }); return (Iterator) result; } @@ -1243,7 +1862,12 @@ public static void convHasStep(HugeGraph graph, HasStep step) { // the caller's bindings or those of a previously cloned traversal. P predicate = copyResolvedPredicate(original.getPredicate()); HasContainer has; - if (localIds && T.id.getAccessor().equals(original.getKey())) { + if (original instanceof LocalSearchHasContainer) { + has = new LocalSearchHasContainer(original.getKey(), predicate); + } else if (original instanceof LocalLabelHasContainer) { + has = new LocalLabelHasContainer(predicate); + } else if (original instanceof LocalIdHasContainer || + localIds && T.id.getAccessor().equals(original.getKey())) { updateLocalIdPredicate(predicate); has = new LocalIdHasContainer(predicate); } else { diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh index 1708eefee0..2aa0782f15 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-codecov-upload-config.sh @@ -38,7 +38,7 @@ expected_files = { "hstore": "${{ env.REPORT_DIR }}/*.xml", }, ".github/workflows/server-tests.yml": { - "build-server": "${{ env.REPORT_DIR }}/*.xml", + "build-server": "${{ env.REPORT_DIR }}/*.xml,hugegraph-server/target/site/jacoco/jacoco.xml", }, } action_pattern = re.compile( 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 f38365b36c..6faf4d4e8a 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 @@ -406,8 +406,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 || @@ -463,7 +464,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/backend/tx/GraphTransactionTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java index d5ed38aa40..cc67b851f0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/backend/tx/GraphTransactionTest.java @@ -17,6 +17,8 @@ package org.apache.hugegraph.backend.tx; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -27,20 +29,24 @@ import org.apache.hugegraph.HugeGraphParams; import org.apache.hugegraph.backend.cache.Cache; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; +import org.apache.hugegraph.backend.query.QueryResultContext; +import org.apache.hugegraph.backend.tx.GraphIndexTransaction.RemoveLeftIndexJob; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.query.Condition; import org.apache.hugegraph.query.ConditionQuery.OptimizedType; import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.query.IdQuery; -import org.apache.hugegraph.backend.query.QueryResultContext; -import org.apache.hugegraph.backend.tx.GraphIndexTransaction.RemoveLeftIndexJob; -import org.apache.hugegraph.job.EphemeralJob; import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.HugeEdge; +import org.apache.hugegraph.structure.HugeElement; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.unit.FakeObjects; import org.apache.tinkerpop.gremlin.structure.T; @@ -50,6 +56,67 @@ public class GraphTransactionTest { + @Test + public void testRangeIndexCleanupWithoutMatchingLabel() throws Exception { + try (FilterFixture fixture = new FilterFixture()) { + ConditionQuery query = Mockito.spy(new ConditionQuery(HugeType.VERTEX)); + query.eq(HugeKeys.LABEL, fixture.vertex.schemaLabel().id()); + query.eq(HugeKeys.LABEL, IdGenerator.of(Long.MAX_VALUE)); + Id field = fixture.graph.propertyKey("name").id(); + ConditionQuery.LeftIndex left = new ConditionQuery.LeftIndex( + Collections.singleton("stale"), field); + Mockito.doReturn(Collections.singleton(left)).when(query) + .getLeftIndexOfElement(fixture.vertex.id()); + Constructor constructor = + RemoveLeftIndexJob.class.getDeclaredConstructor( + ConditionQuery.class, HugeElement.class); + constructor.setAccessible(true); + RemoveLeftIndexJob job = constructor.newInstance(query, fixture.vertex); + Whitebox.setInternalState(job, "tx", fixture.transaction.indexTransaction()); + Method process = RemoveLeftIndexJob.class.getDeclaredMethod( + "processRangeIndexLeft", ConditionQuery.class, HugeElement.class); + process.setAccessible(true); + Assert.assertEquals(0L, process.invoke(job, query, fixture.vertex)); + Mockito.verify(query).removeElementLeftIndex(fixture.vertex.id()); + } + } + + @Test + public void testResolvedEdgeLabelsInBatchResultFilter() throws Exception { + try (FilterFixture fixture = new FilterFixture()) { + Method filter = GraphTransaction.class.getDeclaredMethod( + "rightResultFromIndexQuery", QueryResultContext.class, HugeElement.class); + filter.setAccessible(true); + HugeEdge edge = Mockito.mock(HugeEdge.class); + BaseEdge baseEdge = Mockito.mock(BaseEdge.class); + Mockito.when(edge.element()).thenReturn(baseEdge); + Mockito.when(edge.type()).thenReturn(HugeType.EDGE); + Id first = IdGenerator.of(1L); + Id second = IdGenerator.of(2L); + ConditionQuery query = Mockito.spy(new ConditionQuery(HugeType.EDGE)); + query.optimized(OptimizedType.INDEX); + query.query(Condition.in(HugeKeys.LABEL, Arrays.asList(first, second))); + Mockito.doReturn(false).when(query).test(Mockito.eq(baseEdge), Mockito.isNull()); + + // Multi-label batches must not take the single-label fast path. + Assert.assertEquals(false, filter.invoke(fixture.transaction, + new QueryResultContext(query), edge)); + Mockito.verify(query).test(baseEdge, null); + + // An intersection that resolves to one label can use the fast path. + query.query(Condition.eq(HugeKeys.LABEL, first)); + Assert.assertEquals(true, filter.invoke(fixture.transaction, + new QueryResultContext(query), edge)); + Mockito.verify(query).test(baseEdge, null); + + // An empty intersection must be filtered, not accepted as one label. + query.query(Condition.eq(HugeKeys.LABEL, second)); + Assert.assertEquals(false, filter.invoke(fixture.transaction, + new QueryResultContext(query), edge)); + Mockito.verify(query, Mockito.times(2)).test(baseEdge, null); + } + } + @Test public void testOlapDeletionDoesNotScanBaseVertexEdges() { GraphTransaction transaction = Mockito.mock(GraphTransaction.class, Mockito.CALLS_REAL_METHODS); 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 c69ef7254f..52bd058c6b 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 @@ -3599,6 +3599,265 @@ 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 testPageOutEdgesWithNegativeLabelBeforeLimit() { + this.assertOutEdgeNegativeLabelPages(true); + } + + @Test + public void testPageOutEdgesWithNegativeLabelAfterLimit() { + this.assertOutEdgeNegativeLabelPages(false); + } + + private void assertOutEdgeNegativeLabelPages(boolean filterBeforeLimit) { + Assume.assumeTrue("Not support paging", storeFeatures().supportsQueryByPage()); + HugeGraph graph = graph(); + graph.schema().vertexLabel("pageVertex").useCustomizeNumberId().create(); + graph.schema().edgeLabel("pageExcluded").link("pageVertex", "pageVertex").create(); + graph.schema().edgeLabel("pageIncluded").link("pageVertex", "pageVertex").create(); + Vertex source = graph.addVertex(T.label, "pageVertex", T.id, 1); + Set expected = new HashSet<>(); + for (int i = 0; i < 9; i++) { + Vertex target = graph.addVertex(T.label, "pageVertex", T.id, i + 2); + Edge edge = source.addEdge(i < 6 ? "pageExcluded" : "pageIncluded", target); + if (i >= 6) { + expected.add(edge.id()); + } + } + this.commitTx(); + + Set actual = new HashSet<>(); + Set cursors = new HashSet<>(); + String page = ""; + int pages = 0; + boolean emptyPageWithCursor = false; + do { + GraphTraversal traversal = graph.traversal().V(source.id()) + .outE().has("~page", page); + if (filterBeforeLimit) { + traversal.hasLabel(P.neq("pageExcluded")).limit(1); + } else { + traversal.limit(1).hasLabel(P.neq("pageExcluded")); + } + traversal.asAdmin().applyStrategies(); + Assert.assertNotNull(TraversalUtil.firstPageStep(traversal)); + List edges = traversal.toList(); + Assert.assertTrue(edges.size() <= 1); + for (Edge edge : edges) { + Assert.assertEquals("pageIncluded", edge.label()); + Assert.assertTrue("Duplicate edge across pages", actual.add(edge.id())); + } + page = TraversalUtil.page(traversal); + if (page != null && !page.isEmpty()) { + Assert.assertTrue("Repeated continuation cursor", cursors.add(page)); + emptyPageWithCursor |= edges.isEmpty(); + } + Assert.assertTrue("Paging did not terminate", ++pages <= 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(expected, actual); + // An empty filtered page must not terminate iteration prematurely. + Assert.assertTrue(emptyPageWithCursor); + } + + @Test + public void testLocalEdgeIdAndLabelRepresentations() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("localEV").useCustomizeNumberId().create(); + graph.schema().edgeLabel("localE").link("localEV", "localEV").create(); + Vertex source = graph.addVertex(T.label, "localEV", T.id, 123); + Vertex target = graph.addVertex(T.label, "localEV", T.id, 456); + Edge edge = source.addEdge("localE", target); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(edge), g.E().hasId(edge).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.E().hasId(edge).limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.V(source.id()).outE().hasId(P.within(edge)).limit(10) + .hasLabel(P.neq("other")).toList()); + Id label = graph.edgeLabel("localE").id(); + for (Object value : ImmutableList.of("localE", label, label.asLong())) { + Assert.assertEquals(ImmutableList.of(edge), g.E().has(T.label, value).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.E().has(T.label, value).limit(10) + .hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableList.of(edge), + g.V(source.id()).outE().barrier().has(T.label, P.within(value)).limit(10) + .hasLabel(P.neq("other")).toList()); + } + } + + @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 @@ -7787,6 +8046,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 5add98f071..d0c22e2f22 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 @@ -20,6 +20,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -34,28 +35,30 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.backend.cache.CachedGraphTransaction; -import org.apache.hugegraph.id.Id; -import org.apache.hugegraph.id.Id.IdType; -import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.id.SnowflakeIdGenerator; -import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.backend.page.IdHolderList; import org.apache.hugegraph.backend.page.PageInfo; -import org.apache.hugegraph.query.Condition; -import org.apache.hugegraph.query.ConditionQuery; -import org.apache.hugegraph.query.Query; -import org.apache.hugegraph.serializer.BytesBuffer; import org.apache.hugegraph.backend.store.BackendTable; -import org.apache.hugegraph.backend.Shard; import org.apache.hugegraph.backend.tx.GraphTransaction; +import org.apache.hugegraph.exception.BackendException; +import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; -import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.id.Id.IdType; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.id.SplicingIdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeElement; @@ -64,6 +67,7 @@ import org.apache.hugegraph.testutil.Utils; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.traversal.optimize.ConditionP; +import org.apache.hugegraph.traversal.optimize.HugeGraphStep; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.traversal.optimize.TraversalUtil; import org.apache.hugegraph.type.HugeType; @@ -79,6 +83,10 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.HasStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.map.GraphStep; +import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; +import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; @@ -89,6 +97,7 @@ import org.junit.Test; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; public class VertexCoreTest extends BaseCoreTest { @@ -9255,6 +9264,686 @@ 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 testNegativeLabelAfterReturningToSourceThroughEdge() { + HugeGraph graph = graph(); + initPersonIndex(true); + graph.schema().edgeLabel("fanToPerson").link("fan", "person").create(); + graph.schema().edgeLabel("personToFan").link("person", "fan").create(); + Vertex outFan = graph.addVertex(T.label, "fan", "name", "out-fan", + "age", 20, "city", "Beijing"); + Vertex inFan = graph.addVertex(T.label, "fan", "name", "in-fan", + "age", 20, "city", "Beijing"); + Vertex target = graph.addVertex(T.label, "person", "name", "target", + "age", 20, "city", "Shanghai"); + outFan.addEdge("fanToPerson", target); + target.addEdge("personToFan", inFan); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(outFan), g.V().has("city", "Beijing") + .outE().outV().hasLabel(P.neq("person")).toList()); + Assert.assertEquals(ImmutableList.of(inFan), g.V().has("city", "Beijing") + .inE().inV().hasLabel(P.neq("person")).toList()); + Assert.assertEquals(ImmutableList.of(outFan), g.V().has("city", "Beijing") + .where(__.outE().outV().hasLabel(P.neq("person"))).toList()); + Assert.assertEquals(ImmutableList.of(inFan), g.V().has("city", "Beijing") + .where(__.inE().inV().hasLabel(P.neq("person"))).toList()); + Assert.assertEquals(ImmutableSet.of(outFan, inFan), + ImmutableSet.copyOf(g.V().has("city", "Beijing") + .bothE().bothV().hasLabel(P.neq("person")).toList())); + Assert.assertEquals(ImmutableSet.of(outFan, inFan), + ImmutableSet.copyOf(g.V().has("city", "Beijing") + .where(__.bothE().bothV().hasLabel(P.neq("person"))).toList())); + Assert.assertEquals(ImmutableList.of(outFan), g.V().has("city", "Beijing") + .where(__.outE().barrier().outV().hasLabel(P.neq("person"))).toList()); + } + + @Test + public void testNegativeLabelAfterAdjacencyPreservesSourceCandidates() { + HugeGraph graph = graph(); + initPersonIndex(true); + graph.schema().vertexLabel("returnFan").properties("name", "age", "city") + .useAutomaticId().create(); + graph.schema().edgeLabel("fanLoop").link("returnFan", "returnFan").create(); + graph.schema().edgeLabel("personLoop").link("person", "person").create(); + Vertex fan = graph.addVertex(T.label, "returnFan", "name", "loop-fan", + "age", 20, "city", "Beijing"); + Vertex person = graph.addVertex(T.label, "person", "name", "loop-person", + "age", 20, "city", "Beijing"); + fan.addEdge("fanLoop", fan); + Edge personEdge = person.addEdge("personLoop", person); + this.commitTx(); + + GraphTraversalSource g = graph.traversal(); + // The existing source query uses only indexed labels. #3201 tracks + // complete cross-label candidates; a later label must not change this + // source query's behavior solely because it follows an adjacency step. + Set source = g.V().has("city", "Beijing").toSet(); + Assert.assertEquals(ImmutableSet.of(person), source); + Assert.assertEquals(source, g.V().has("city", "Beijing").out().toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel("person").toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).dedup().toSet()); + Assert.assertEquals(ImmutableList.of(person), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).order().by("name").toList()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).tail(1).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).fold().unfold().toSet()); + Assert.assertEquals(ImmutableList.of(20), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).values("age").is(P.gt(1)).toList()); + Assert.assertEquals(ImmutableList.of(1), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).constant(1).toList()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).sample(2).toSet()); + Assert.assertEquals(ImmutableMap.of("person", ImmutableList.of(person)), g.V() + .has("city", "Beijing").out().hasLabel(P.neq("author")).group().by(T.label).next()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).aggregate("x").toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).cyclicPath().toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().in().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .both().both().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .out().inE().outV().hasLabel(P.neq("author")).toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .where(__.out().in().hasLabel(P.neq("author"))).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).dedup().toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).tail(1).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge), g.V().has("city", "Beijing") + .outE().hasLabel(P.neq("knows")).fold().unfold().toSet()); + Assert.assertEquals(source, g.V().has("city", "Beijing") + .where(__.out().hasLabel(P.neq("author")).tail(1)).toSet()); + } + + @Test + public void testChildSourceBeforeParentNegativeLabel() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + Vertex fan = graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Set expected = g.V().hasLabel(P.neq("author")) + .has("city", "Beijing").id().toSet(); + Assert.assertEquals(4, expected.size()); + Assert.assertTrue(expected.contains(fan.id())); + + List ids = g.inject(1).union(__.V().has("city", "Beijing")) + .hasLabel(P.neq("author")).id().toList(); + Assert.assertEquals(4, ids.size()); + Assert.assertEquals(expected, ImmutableSet.copyOf(ids)); + ids = g.inject(1).local(__.union(__.V().has("city", "Beijing"))) + .hasLabel(P.neq("author")).id().toList(); + Assert.assertEquals(4, ids.size()); + Assert.assertEquals(expected, ImmutableSet.copyOf(ids)); + } + + @Test + public void testLocalUuidIdBeforeNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("uuid").useCustomizeUuidId().create(); + UUID first = UUID.fromString("835e1153-9281-4957-8691-cf79258e90eb"); + UUID second = UUID.fromString("835e1153-9281-4957-8691-cf79258e90ec"); + Vertex one = graph.addVertex(T.label, "uuid", T.id, first); + Vertex two = graph.addVertex(T.label, "uuid", T.id, second); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(one), g.V().hasId(first).toList()); + Assert.assertEquals(ImmutableList.of(one), + g.V().hasId(first).limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(P.within(first, second)).limit(10) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableList.of(one), + g.V(first).hasId(P.within(first, second)).limit(10) + .hasLabel(P.neq("other")).toList()); + Assert.assertTrue(g.V(second).hasId(first).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + P predicate = P.eq(first).or(P.eq(second)); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(predicate).limit(10) + .hasLabel(P.neq("other")).toSet()); + // A reused predicate must still accept the caller's original UUIDs. + Assert.assertTrue(predicate.test(first)); + Assert.assertTrue(predicate.test(second)); + Assert.assertEquals(ImmutableList.of(two), + g.V().hasId(P.without(first)).limit(10) + .hasLabel(P.neq("other")).toList()); + } + + @Test + public void testLocalElementIdAndLabelRepresentations() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("localV").useCustomizeNumberId().create(); + Vertex one = graph.addVertex(T.label, "localV", T.id, 123); + Vertex two = graph.addVertex(T.label, "localV", T.id, 456); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(one), g.V().hasId(one).toList()); + Assert.assertEquals(ImmutableList.of(one), + g.V().hasId(one).limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(P.within(one, two)).limit(10) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableList.of(one), + g.V(one.id()).hasId(P.within(one, two)).limit(10) + .hasLabel(P.neq("other")).toList()); + Assert.assertTrue(g.V(two.id()).hasId(one).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + P ids = P.eq(one).or(P.eq(two)); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(ids).limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(ids.test(one)); + Id label = graph.vertexLabel("localV").id(); + Set expected = ImmutableSet.of(one, two); + for (Object value : ImmutableList.of("localV", label, label.asLong())) { + Assert.assertEquals(expected, g.V().has(T.label, value).toSet()); + Assert.assertEquals(expected, g.V().has(T.label, value).limit(10) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(expected, g.inject(1).local(__.union(__.V().has(T.label, value))) + .hasLabel(P.neq("other")).toSet()); + } + P labels = P.eq((Object) label).or(P.eq("localV")); + GraphTraversal traversal = g.V().has(T.label, labels) + .limit(10).hasLabel(P.neq("other")); + traversal.asAdmin().applyStrategies(); + GraphTraversal.Admin clone = traversal.asAdmin().clone(); + Assert.assertEquals(expected, traversal.toSet()); + Assert.assertEquals(expected, clone.toSet()); + traversal.asAdmin().reset(); + Assert.assertEquals(expected, traversal.toSet()); + Assert.assertTrue(labels.test(label)); + Assert.assertEquals(expected, g.V().has(T.label, P.eq((Object) label).and(P.neq(-1L))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(expected, g.V().has(T.label, P.within(label, IdGenerator.of(-1L))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(expected, g.V().has(T.label, P.without(IdGenerator.of(-1L))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(g.V().has(T.label, IdGenerator.of(-1L)).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + Assert.assertTrue(g.V().has(T.label, P.without(label)).limit(10) + .hasLabel(P.neq("other")).toList().isEmpty()); + } + + @Test + public void testNotLabelContextKeepsUnindexedCandidates() { + HugeGraph graph = graph(); + initPersonIndex(true); + init5Persons(); + Vertex fan = graph.addVertex(T.label, "fan", "name", "unindexed-city-fan", + "age", 20, "city", "Beijing"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Set expected = g.V().hasLabel(P.neq("author")).has("city", "Beijing").toSet(); + Assert.assertEquals(4, expected.size()); + Assert.assertTrue(expected.contains(fan)); + Assert.assertEquals(expected, g.V().has("city", "Beijing") + .not(__.hasLabel("author")).toSet()); + Assert.assertEquals(expected, g.V().has("city", "Beijing") + .not(__.or(__.hasLabel("author"), __.hasLabel("missing"))) + .toSet()); + Assert.assertEquals(expected, g.inject(1).union(__.V().has("city", "Beijing")) + .not(__.hasLabel("author")).toSet()); + } + + @Test + public void testNegativeLabelAfterElementChange() { + HugeGraph graph = graph(); + graph.schema().propertyKey("city").asText().ifNotExist().create(); + graph.schema().vertexLabel("indexed").properties("city").useAutomaticId().create(); + graph.schema().vertexLabel("target").useAutomaticId().create(); + graph.schema().indexLabel("byCity").onV("indexed").by("city").secondary().create(); + graph.schema().edgeLabel("link").link("indexed", "target").create(); + Vertex source = graph.addVertex(T.label, "indexed", "city", "Beijing"); + Vertex target = graph.addVertex(T.label, "target"); + source.addEdge("link", target); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + for (GraphTraversal query : ImmutableList.of( + g.V().has("city", "Beijing").out().hasLabel(P.neq("indexed")), + g.V().has("city", "Beijing").out().where(__.not(__.hasLabel("indexed"))))) { + query.asAdmin().applyStrategies(); + HugeGraphStep step = (HugeGraphStep) query.asAdmin().getStartStep(); + Assert.assertTrue(step.getHasContainers().stream() + .anyMatch(h -> h.getKey().equals("city"))); + Assert.assertEquals(ImmutableList.of(target), query.toList()); + } + Assert.assertEquals(ImmutableList.of(source), g.V().has("city", "Beijing").as("a") + .out().select("a").hasLabel(P.neq("target")).toList()); + Assert.assertEquals(ImmutableList.of(source), g.V().has("city", "Beijing") + .out().path().unfold().hasLabel(P.neq("target")).toList()); + } + + @Test + public void testLocalContainsBeforeNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("age").asInt().ifNotExist().create(); + graph.schema().propertyKey("key").asText().ifNotExist().create(); + graph.schema().propertyKey("value").asInt().ifNotExist().create(); + graph.schema().vertexLabel("containsV").properties("age", "key", "value") + .nullableKeys("age", "key", "value").useAutomaticId().create(); + graph.schema().edgeLabel("containsE").link("containsV", "containsV") + .properties("age", "key", "value").nullableKeys("age", "key", "value").create(); + Vertex one = graph.addVertex(T.label, "containsV", "age", 20); + Vertex two = graph.addVertex(T.label, "containsV", "age", 30); + Vertex misleading = graph.addVertex(T.label, "containsV", "key", "age", "value", 99); + Edge first = one.addEdge("containsE", two, "age", 20); + Edge second = one.addEdge("containsE", misleading, "key", "age", "value", 99); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasValue(20).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V(one.id(), misleading.id()).hasKey("age") + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasKey("age").hasId(one.id()).hasValue(20) + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(two), + g.V(one.id()).out().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.E().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.E().hasValue(20).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.E(first.id(), second.id()).hasKey("age") + .hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.V(one.id()).outE().hasKey("age").hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(first), + g.V(one.id()).outE().hasValue(20).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(g.V().hasValue(-1).hasLabel(P.neq("other")).toList().isEmpty()); + Assert.assertTrue(g.V().hasKey("age").hasLabel(P.neq("containsV")).toList().isEmpty()); + Assert.assertThrows(IllegalArgumentException.class, () -> + g.V().hasKey("age", "key").hasLabel(P.neq("other")).toList()); + Assert.assertThrows(IllegalArgumentException.class, () -> + g.V().hasValue(20, 30).hasLabel(P.neq("other")).toList()); + GraphTraversal.Admin traversal = g.V().hasKey("age") + .hasLabel(P.neq("other")).asAdmin(); + traversal.applyStrategies(); + GraphTraversal.Admin clone = traversal.clone(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + Assert.assertEquals(ImmutableSet.of(one, two), clone.toSet()); + traversal.reset(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + } + + @Test + public void testLocalConnectiveStringIds() { + HugeGraph graph = graph(); + graph.schema().vertexLabel("stringV").useCustomizeStringId().create(); + graph.schema().edgeLabel("stringE").link("stringV", "stringV").create(); + Vertex one = graph.addVertex(T.label, "stringV", T.id, "a"); + Vertex two = graph.addVertex(T.label, "stringV", T.id, "b"); + Edge edge = one.addEdge("stringE", two); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + P either = P.eq("a").or(P.eq("b")); + Assert.assertEquals(ImmutableSet.of(one), g.V().hasId("a").toSet()); + Assert.assertEquals(ImmutableSet.of(two), g.V().hasId("b").toSet()); + Assert.assertEquals(ImmutableSet.of(one, two), + g.V().hasId(either).limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasId(P.eq("a").and(P.neq("b"))) + .limit(10).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(one), + g.V(one.id()).hasId(either).hasLabel(P.neq("other")).toSet()); + Assert.assertTrue(either.test("a")); + Assert.assertTrue(either.test("b")); + Assert.assertEquals(ImmutableSet.of(one), + g.V().hasId(P.within("a", "b").and(P.neq("b"))) + .limit(10).hasLabel(P.neq("other")).toSet()); + P edgeIds = P.eq(edge.id().toString()).or(P.eq("missing")); + Assert.assertEquals(ImmutableSet.of(edge), + g.E().hasId(edgeIds).hasLabel(P.neq("other")).toSet()); + Assert.assertEquals(ImmutableSet.of(edge), + g.V(one.id()).outE().hasId(edgeIds).hasLabel(P.neq("other")).toSet()); + GraphTraversal.Admin traversal = g.V().hasId(either) + .hasLabel(P.neq("other")).asAdmin(); + traversal.applyStrategies(); + GraphTraversal.Admin clone = traversal.clone(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + Assert.assertEquals(ImmutableSet.of(one, two), clone.toSet()); + traversal.reset(); + Assert.assertEquals(ImmutableSet.of(one, two), traversal.toSet()); + } + + @Test + public void testRangeIndexAfterElementChangeWithTerminalSteps() { + HugeGraph graph = graph(); + graph.schema().propertyKey("age").asInt().ifNotExist().create(); + graph.schema().vertexLabel("indexed").properties("age").useAutomaticId().create(); + graph.schema().vertexLabel("unindexed").properties("age").useAutomaticId().create(); + graph.schema().vertexLabel("target").useAutomaticId().create(); + graph.schema().indexLabel("byAge").onV("indexed").by("age").range().create(); + graph.schema().edgeLabel("link").link("indexed", "target").create(); + Vertex source = graph.addVertex(T.label, "indexed", "age", 60); + graph.addVertex(T.label, "unindexed", "age", 60); + Vertex target = graph.addVertex(T.label, "target"); + source.addEdge("link", target); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + List> queries = ImmutableList.of( + g.V().has("age", P.gte(60)).out().hasLabel(P.neq("indexed")).count(), + g.V().has("age", P.gte(60)).out().where(__.not(__.hasLabel("indexed"))).count(), + g.V().has("age", P.gte(60)).out().hasLabel(P.neq("indexed")).id(), + g.V().has("age", P.gte(60)).out().hasLabel(P.neq("indexed")).label()); + List expected = ImmutableList.of(1L, 1L, target.id(), "target"); + for (int i = 0; i < queries.size(); i++) { + GraphTraversal query = queries.get(i); + query.asAdmin().applyStrategies(); + HugeGraphStep step = (HugeGraphStep) query.asAdmin().getStartStep(); + Assert.assertTrue(step.getHasContainers().stream() + .anyMatch(h -> h.getKey().equals("age"))); + Assert.assertEquals(ImmutableList.of(expected.get(i)), query.toList()); + } + } + + @Test + public void testRepeatSiblingNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("city").asText().ifNotExist().create(); + graph.schema().vertexLabel("indexed").properties("city").useAutomaticId().create(); + graph.schema().vertexLabel("unindexed").properties("city").useAutomaticId().create(); + graph.schema().indexLabel("byCity").onV("indexed").by("city").secondary().create(); + Vertex indexed = graph.addVertex(T.label, "indexed", "city", "Beijing"); + Vertex unindexed = graph.addVertex(T.label, "unindexed", "city", "Beijing"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Set expected = ImmutableSet.of(indexed, unindexed); + Assert.assertEquals(expected, + g.V(indexed.id()).repeat(__.V().has("city", "Beijing")) + .until(__.hasLabel(P.neq("author"))).toSet()); + List emitted = g.V(indexed.id()).repeat(__.V().has("city", "Beijing")) + .emit(__.hasLabel(P.neq("author"))).times(2).toList(); + Assert.assertEquals(6, emitted.size()); + Assert.assertEquals(expected, ImmutableSet.copyOf(emitted)); + } + + @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(); @@ -9496,6 +10185,306 @@ public void testEnhanceTextMatch() { Assert.assertTrue(vertices.contains(vertex4)); } + @Test + public void testPositiveLabelBeforeUnsafeChildUsesLabelQuery() { + HugeGraph graph = graph(); + graph.schema().propertyKey("unindexedAge").asInt().create(); + graph.schema().vertexLabel("sourceV").properties("unindexedAge") + .useAutomaticId().create(); + graph.schema().vertexLabel("targetV").useAutomaticId().create(); + graph.schema().vertexLabel("unrelatedV").useAutomaticId().create(); + graph.schema().vertexLabel("withoutLabelIndex").useAutomaticId() + .enableLabelIndex(false).create(); + graph.schema().edgeLabel("positiveLink").link("sourceV", "targetV").create(); + Vertex source = graph.addVertex(T.label, "sourceV", "unindexedAge", 18); + Vertex target = graph.addVertex(T.label, "targetV"); + Vertex unindexed = graph.addVertex(T.label, "withoutLabelIndex"); + Edge edge = source.addEdge("positiveLink", target); + for (int i = 0; i < 5; i++) { + graph.addVertex(T.label, "unrelatedV"); + } + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Id label = graph.vertexLabel("sourceV").id(); + for (P predicate : new P[]{P.eq("sourceV"), P.eq(label), P.eq(label.asLong()), + P.within("sourceV", "targetV"), P.within(label, "sourceV")}) { + GraphTraversal query = g.V().has(T.label, predicate) + .has("unindexedAge", 18) + .where(__.hasLabel(P.neq("targetV"))); + query.asAdmin().applyStrategies(); + HugeGraphStep step = (HugeGraphStep) query.asAdmin().getStartStep(); + Assert.assertEquals(1, step.getHasContainers().size()); + Assert.assertEquals(T.label.getAccessor(), step.getHasContainers().get(0).getKey()); + Assert.assertEquals(ImmutableList.of(source), query.toList()); + GraphTraversal downstream = g.V().has(T.label, predicate) + .has("unindexedAge", 18) + .where(__.out().hasLabel(P.neq("sourceV"))); + downstream.asAdmin().applyStrategies(); + HugeGraphStep downstreamStep = + (HugeGraphStep) downstream.asAdmin().getStartStep(); + Assert.assertEquals(2, downstreamStep.getHasContainers().size()); + Assert.assertThrows(NoIndexException.class, downstream::toList); + Assert.assertEquals(ImmutableList.of(source), g.V(source.id(), target.id()) + .has(T.label, predicate).where(__.out().hasLabel(P.neq("sourceV"))).toList()); + } + Assert.assertEquals(Long.valueOf(1L), g.V().hasLabel("sourceV") + .where(__.out().hasLabel(P.neq("sourceV"))).count().next()); + Assert.assertTrue(g.V().hasLabel(P.within(Collections.emptyList())) + .where(__.out().hasLabel(P.neq("sourceV"))).toList().isEmpty()); + Assert.assertTrue(g.V().hasLabel("sourceV").hasLabel("targetV") + .where(__.out().hasLabel(P.neq("sourceV"))).toList().isEmpty()); + // Unknown labels are evaluated locally only when fallback is required. + Assert.assertEquals(ImmutableList.of(source), g.V().hasLabel(P.within("sourceV", "missing")) + .where(__.hasLabel(P.neq("targetV"))).toList()); + Assert.assertThrows(IllegalArgumentException.class, + () -> g.V().hasLabel(P.within("sourceV", "missing")) + .where(__.out().hasLabel(P.neq("sourceV"))).toList()); + Assert.assertTrue(g.V().hasLabel("missing").hasLabel(P.neq("sourceV")).toList().isEmpty()); + Assert.assertEquals(ImmutableList.of(unindexed), g.V().hasLabel("withoutLabelIndex") + .hasLabel(P.neq("sourceV")).toList()); + Assert.assertEquals(ImmutableList.of(target), g.V(source.id()).out().hasLabel("targetV") + .hasLabel(P.neq("sourceV")).toList()); + Assert.assertEquals(ImmutableList.of(target), g.V(source.id()).out().has(T.label, + graph.vertexLabel("targetV").id()).hasLabel(P.neq("sourceV")).toList()); + Id edgeLabel = graph.edgeLabel("positiveLink").id(); + Assert.assertEquals(ImmutableList.of(edge), g.E().has(T.label, edgeLabel) + .limit(10).hasLabel(P.neq("other")).toList()); + Assert.assertEquals(ImmutableList.of(edge), g.E(edge.id()).has(T.label, edgeLabel) + .hasLabel(P.neq("other")).toList()); + // A selective label query must not consume unrelated vertices' capacity. + if ("rocksdb".equals(graph.backend())) { + long old = Query.defaultCapacity(3L); + try { + Assert.assertEquals(ImmutableList.of(source), g.V().hasLabel("sourceV") + .where(__.out().hasLabel(P.neq("sourceV"))).toList()); + } finally { + Query.defaultCapacity(old); + } + } + } + + @Test + public void testUnindexedPropertyBeforeNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("unindexedProp").asText().create(); + graph.schema().vertexLabel("scanDoc").properties("unindexedProp") + .useAutomaticId().create(); + graph.schema().vertexLabel("scanExcluded").properties("unindexedProp") + .useAutomaticId().create(); + Vertex match = graph.addVertex(T.label, "scanDoc", "unindexedProp", "x"); + graph.addVertex(T.label, "scanDoc", "unindexedProp", "y"); + graph.addVertex(T.label, "scanExcluded", "unindexedProp", "x"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x").toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .out().hasLabel(P.neq("scanExcluded")).toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .out().hasLabel(P.neq("scanExcluded")).dedup().toList()); + for (GraphTraversal query : new GraphTraversal[]{ + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).tail(1), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .values("unindexedProp").is("y"), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .fold().unfold(), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).constant(1), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).sample(2), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .group().by(T.label), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")) + .aggregate("x"), + g.V().has("unindexedProp", "x").out().hasLabel(P.neq("scanExcluded")).simplePath(), + g.V().has("unindexedProp", "x").outE().hasLabel(P.neq("knows")).tail(1), + g.V().has("unindexedProp", "x").outE().hasLabel(P.neq("knows")).fold().unfold(), + g.V().has("unindexedProp", "x") + .where(__.out().hasLabel(P.neq("scanExcluded")).tail(1))}) { + Assert.assertThrows(NoIndexException.class, query::toList); + } + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .outE().hasLabel(P.neq("knows")).toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .outE().hasLabel(P.neq("knows")).dedup().toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .where(__.out().hasLabel(P.neq("scanExcluded"))).toList()); + Assert.assertThrows(NoIndexException.class, + () -> g.V().has("unindexedProp", "x") + .not(__.out().hasLabel("scanExcluded")).toList()); + Assert.assertEquals(ImmutableList.of(match.id()), + g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).id().toList()); + Assert.assertEquals(Long.valueOf(1L), g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).count().next()); + Assert.assertEquals(ImmutableList.of(match.id()), + g.V(match.id()).has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).id().toList()); + long old = Query.defaultCapacity(1L); + try { + if ("rocksdb".equals(graph.backend())) { + // RocksDB's iterator counts candidates, not just final matches. + Assert.assertThrows(LimitExceedException.class, + () -> g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).toList()); + } else if ("memory".equals(graph.backend())) { + // The test-only memory backend does not enforce scan capacity. + Assert.assertEquals(ImmutableList.of(match.id()), + g.V().has("unindexedProp", "x") + .hasLabel(P.neq("scanExcluded")).id().toList()); + } + } finally { + Query.defaultCapacity(old); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testUnboundLocalSearchUsesGraphAnalyzer() { + HugeGraph graph = graph(); + graph.schema().propertyKey("body").asText().create(); + graph.schema().vertexLabel("runtimeDoc").properties("body").useAutomaticId().create(); + Vertex alpha = graph.addVertex(T.label, "runtimeDoc", "body", "alpha"); + Vertex alphabet = graph.addVertex(T.label, "runtimeDoc", "body", "alphabet"); + this.commitTx(); + for (P predicate : new P[]{Text.contains("(alpha)"), + Text.contains("(alpha)").or(P.eq("beta")).and(P.neq("excluded"))}) { + GraphTraversal.Admin traversal = __.V().has("body", predicate) + .limit(10).hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = new HugeGraphStep<>( + (GraphStep) traversal.getStartStep()); + traversal.removeStep(0); + traversal.addStep(0, source); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(alpha)); + Assert.assertFalse(local.test(alphabet)); + Assert.assertTrue(local.clone().test(alpha)); + } + } + + @Test + public void testSearchBeforeDownstreamNegativeLabel() { + HugeGraph graph = graph(); + graph.schema().propertyKey("body").asText().create(); + graph.schema().vertexLabel("doc").properties("body").useAutomaticId().create(); + graph.schema().vertexLabel("unindexedDoc").properties("body").useAutomaticId().create(); + graph.schema().indexLabel("docByBody").onV("doc").by("body").search().create(); + Vertex alpha = graph.addVertex(T.label, "doc", "body", "alpha"); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + for (String text : ImmutableList.of("(alpha)", "(alpha|beta)", "alpha", "alpha beta")) { + Assert.assertEquals(ImmutableList.of(alpha.id()), + g.V().hasLabel("doc") + .has("body", Text.contains(text)).id().toList()); + Assert.assertEquals(ImmutableList.of(alpha.id()), + g.V().hasLabel("doc") + .has("body", Text.contains(text)).limit(10) + .has(T.label, P.neq("author")).id().toList()); + } + Assert.assertEquals(ImmutableList.of(alpha.id()), + g.V().has("body", Text.contains("(alpha)").or(P.eq("beta"))) + .limit(10).has(T.label, P.neq("author")).id().toList()); + Vertex unindexed = graph.addVertex(T.label, "unindexedDoc", "body", "alpha"); + this.commitTx(); + List ids = g.V().has("body", Text.contains("(alpha|beta)")) + .limit(10).has(T.label, P.neq("author")).id().toList(); + Assert.assertEquals(2, ids.size()); + Assert.assertEquals(ImmutableSet.of(alpha.id(), unindexed.id()), ImmutableSet.copyOf(ids)); + } + + @Test + public void testNegativeLabelPreservesRangeAndAggregate() { + HugeGraph graph = graph(); + initPersonIndex(true); + graph.schema().vertexLabel("author").properties("city").nullableKeys("city").append(); + graph.addVertex(T.label, "person", "name", "a", "city", "Beijing", "age", 20); + Vertex excluded = graph.addVertex(T.label, "author", "name", "b", + "id", 1, "age", 20, "city", "Beijing"); + Vertex included = graph.addVertex(T.label, "fan", "name", "c", + "age", 20, "city", "Beijing"); + graph.addVertex(T.label, "person", "name", "d", "city", "Beijing", "age", 20); + this.commitTx(); + GraphTraversalSource g = graph.traversal(); + Assert.assertEquals(ImmutableList.of(included.id()), + g.V().has("city", "Beijing").order().by("name").range(1, 3) + .has(T.label, P.neq("author")).id().toList()); + GraphTraversal traversal = g.V().has("city", "Beijing") + .order().by("name").range(1, 3).aggregate("seen") + .has(T.label, P.neq("author")); + Assert.assertEquals(ImmutableList.of(included), traversal.toList()); + Collection seen = traversal.asAdmin().getSideEffects().get("seen"); + Assert.assertEquals(2, seen.size()); + Assert.assertTrue(seen.contains(excluded)); + Assert.assertTrue(seen.contains(included)); + } + + @Test + public void testPageBeforeDownstreamNegativeLabel() { + Assume.assumeTrue("Not support paging", storeFeatures().supportsQueryByPage()); + HugeGraph graph = graph(); + Set expected = new HashSet<>(); + for (int i = 0; i < 5; i++) { + expected.add(graph.addVertex(T.label, "person", "name", "page-" + i, + "city", "Beijing", "age", 20).id()); + } + // More excluded vertices than matches also exercises empty filtered + // pages whose backend cursor must still be followed. + for (int i = 0; i < 10; i++) { + graph.addVertex(T.label, "author", "id", i, "name", "excluded-" + i); + } + this.commitTx(); + assertNegativeLabelPages(graph, expected, false); + assertNegativeLabelPages(graph, expected, true); + } + + private static void assertNegativeLabelPages(HugeGraph graph, Set expected, + boolean localFilter) { + Set actual = new HashSet<>(); + String page = ""; + int pages = 0; + do { + GraphTraversal traversal = graph.traversal().V(); + if (localFilter) { + traversal.hasLabel("person").has("age", 20); + } + traversal.has("~page", page).limit(2).has(T.label, P.neq("author")); + traversal.asAdmin().applyStrategies(); + Assert.assertNotNull(TraversalUtil.firstPageStep(traversal)); + List vertices = traversal.toList(); + Assert.assertTrue(vertices.size() <= 2); + for (Vertex vertex : vertices) { + Assert.assertTrue(actual.add(vertex.id())); + } + page = TraversalUtil.page(traversal); + Assert.assertTrue(++pages <= 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(expected, actual); + Assert.assertTrue(pages >= 3); + } + + @Test + public void testSelectedLabelsExcludeIndexedInterference() { + HugeGraph graph = graph(); + Set expected = new HashSet<>(); + for (String label : ImmutableList.of("selectedA", "selectedB", "outside")) { + graph.schema().vertexLabel(label).properties("age").useAutomaticId().create(); + graph.schema().indexLabel(label + "ByAge").onV(label).by("age").secondary().create(); + Vertex vertex = graph.addVertex(T.label, label, "age", 20); + if (!label.equals("outside")) { + expected.add(vertex.id()); + } + } + this.commitTx(); + List ids = graph.traversal().V().hasLabel("selectedA", "selectedB") + .has("age", 20).id().toList(); + Assert.assertEquals(2, ids.size()); + Assert.assertEquals(expected, new HashSet<>(ids)); + } + private void init10VerticesAndCommit() { this.init10Vertices(); this.commitTx(); 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 54a49c67aa..002ebd55ea 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 @@ -17,6 +17,12 @@ package org.apache.hugegraph.traversal.optimize; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -25,18 +31,25 @@ import java.util.Set; import org.apache.hugegraph.HugeGraph; +import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; -import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.struct.schema.PropertyKey; +import org.apache.hugegraph.struct.schema.VertexLabel; +import org.apache.hugegraph.structure.BaseVertex; +import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.tinkerpop.gremlin.process.traversal.GType; +import org.apache.tinkerpop.gremlin.process.traversal.NotP; import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.Scope; import org.apache.tinkerpop.gremlin.process.traversal.Step; import org.apache.tinkerpop.gremlin.process.traversal.TextP; import org.apache.tinkerpop.gremlin.process.traversal.Traversal; import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.Traverser; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.process.traversal.step.HasContainerHolder; @@ -48,9 +61,12 @@ import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep; import org.apache.tinkerpop.gremlin.process.traversal.step.util.HasContainer; import org.apache.tinkerpop.gremlin.process.traversal.strategy.optimization.InlineFilterStrategy; +import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP; import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.VertexProperty; +import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph; import org.junit.Test; import org.mockito.Mockito; @@ -279,6 +295,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() @@ -377,6 +458,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); @@ -435,6 +543,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 testLabelContainersWithImmutableList() { assertNullHostileLabelCollection(List.of("person")); @@ -494,6 +669,677 @@ public void testIsPositiveLabelContainer() { P.within(null, "person")))); } + @Test + public void testPageIsConsumedBeforeUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Traversal.Admin traversal = traversal( + __.V().has("~page", "").limit(2).has(T.label, P.neq("author")), graph); + HugeGraphStep graphStep = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(graphStep, traversal); + Assert.assertTrue(graphStep.queryInfo().paging()); + Assert.assertFalse(hasStepExists(traversal, "~page")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + + traversal = traversal( + __.V().outE().has("~page", "").limit(2).has(T.label, P.neq("knows")), graph); + HugeVertexStep vertexStep = replaceVertexStep(traversal); + TraversalUtil.extractHasContainer(vertexStep, traversal); + Assert.assertTrue(vertexStep.queryInfo().paging()); + Assert.assertFalse(hasStepExists(traversal, "~page")); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + } + + @Test + public void testLocalSearchWithoutGraphUsesRuntimeAnalyzer() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + HugeGraph other = Mockito.mock(HugeGraph.class); + Mockito.when(other.searchPredicate("(alpha)")).thenReturn("different"::equals); + for (P search : new P[]{Text.contains("(alpha)"), + Text.contains("(alpha)").or(P.eq("beta")).and(P.neq("excluded"))}) { + for (GraphTraversal chain : new GraphTraversal[]{ + __.V().has("body", search).limit(10).hasLabel(P.neq("author")), + __.outE().has("body", search).limit(10).hasLabel(P.neq("knows"))}) { + Traversal.Admin traversal = chain.asAdmin(); + Step source; + if (traversal.getStartStep() instanceof GraphStep) { + HugeGraphStep step = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(step, traversal); + source = step; + } else { + HugeVertexStep step = replaceVertexStep(traversal); + TraversalUtil.extractHasContainer(step, traversal); + source = step; + } + Assert.assertTrue(((QueryHolder) source).getHasContainers().isEmpty()); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertEquals(search, local.getPredicate()); + Assert.assertFalse(Text.contains("(alpha)").test("alpha")); + local = roundTrip(local); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertFalse(local.test(searchVertex(graph, "alphabet"))); + Assert.assertEquals(search instanceof ConnectiveP, + local.test(searchVertex(graph, "beta"))); + Assert.assertFalse(local.test(searchVertex(graph, "excluded"))); + HasContainer clone = local.clone(); + Assert.assertFalse(clone.test(searchVertex(other, "alpha"))); + Assert.assertTrue(clone.test(searchVertex(other, "different"))); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + } + } + } + + @Test + public void testPositiveLabelPushdownBeforeUnsafeChild() { + HugeGraph graph = positiveLabelGraph(); + for (P label : new P[]{P.eq("person"), P.within("person", "fan"), + P.within(Collections.emptyList())}) { + Traversal.Admin traversal = traversal(__.V().has(T.label, label).has("age", 18) + .where(__.hasLabel(P.neq("software"))), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertEquals(1, source.getHasContainers().size()); + Assert.assertEquals(label, source.getHasContainers().get(0).getPredicate()); + Assert.assertFalse(hasStepExists(traversal, T.label.getAccessor())); + Assert.assertTrue(hasStepExists(traversal, "age")); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertEquals(1, source.getHasContainers().size()); + } + } + + @Test + public void testPositiveLabelPushdownDoesNotCrossRange() { + Traversal.Admin traversal = traversal(__.V().hasLabel("person").limit(2) + .hasLabel("fan").hasLabel(P.neq("software")), positiveLabelGraph()); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertEquals(1, source.getHasContainers().size()); + Assert.assertEquals(P.eq("person"), source.getHasContainers().get(0).getPredicate()); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + } + + @Test + public void testPositiveLabelFallbackKeepsUnsupportedCandidatesLocal() { + HugeGraph graph = positiveLabelGraph(); + VertexLabel disabled = new VertexLabel(graph, IdGenerator.of(3L), "disabled"); + disabled.enableLabelIndex(false); + Mockito.when(graph.vertexLabel("disabled")).thenReturn(disabled); + Mockito.when(graph.vertexLabel("missing")) + .thenThrow(new IllegalArgumentException("Undefined vertex label")); + for (P label : new P[]{P.eq("disabled"), P.eq("missing"), + P.eq(IdGenerator.of(-1L)), P.within("person", IdGenerator.of(-1L)), + P.within("person", "missing"), P.eq(Collections.singletonList("person"))}) { + Traversal.Admin traversal = traversal(__.V().has(T.label, label) + .hasLabel(P.neq("software")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertTrue(source.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, T.label.getAccessor())); + } + } + + private static HugeGraph positiveLabelGraph() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.vertexLabel("person")) + .thenReturn(new VertexLabel(graph, IdGenerator.of(1L), "person")); + Mockito.when(graph.vertexLabel("fan")) + .thenReturn(new VertexLabel(graph, IdGenerator.of(2L), "fan")); + return graph; + } + + @Test + public void testLocalSearchPreservesPredicateAndSerialization() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + for (P predicate : new P[]{Text.contains("(alpha)"), + Text.contains("(alpha)").or(P.eq("beta")).and(P.neq("excluded"))}) { + Traversal.Admin traversal = traversal( + __.V().has("body", predicate).limit(10).hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + HasContainer original = ((HasStep) source.getNextStep()).getHasContainers().get(0); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertEquals(predicate, local.getPredicate()); + Assert.assertEquals(original.hashCode(), local.hashCode()); + Assert.assertEquals(original.toString(), local.toString()); + HasContainer restored = roundTrip(local); + Assert.assertTrue(restored.test(searchVertex(graph, "alpha"))); + Assert.assertEquals(predicate instanceof ConnectiveP, + restored.test(searchVertex(graph, "beta"))); + Assert.assertFalse(restored.test(searchVertex(graph, "excluded"))); + // A warmed matcher must not serialize its graph/analyzer closure. + restored = roundTrip(restored); + Assert.assertEquals(predicate, restored.getPredicate()); + Assert.assertTrue(restored.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(restored.clone().test(searchVertex(graph, "alpha"))); + + HugeGraph otherGraph = Mockito.mock(HugeGraph.class); + Mockito.when(otherGraph.searchPredicate("(alpha)")).thenReturn("different"::equals); + Assert.assertFalse(restored.test(searchVertex(otherGraph, "alpha"))); + Assert.assertTrue(restored.test(searchVertex(otherGraph, "different"))); + Assert.assertTrue(restored.clone().test(searchVertex(graph, "alpha"))); + } + } + + @Test + public void testLocalSearchSurvivesPredicateConversion() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("body")).thenReturn(propertyKey(1L, "body", DataType.TEXT)); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + Traversal.Admin traversal = traversal( + __.V().has("body", Text.contains("(alpha)")) + .limit(10).hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasStep filter = (HasStep) source.getNextStep(); + TraversalUtil.convHasStep(graph, filter); + Vertex vertex = searchVertex(graph, "alpha"); + Assert.assertTrue(HasContainer.testAll(vertex, filter.getHasContainers())); + Assert.assertFalse(HasContainer.testAll(searchVertex(graph, "(alpha)"), + filter.getHasContainers())); + } + + @Test + public void testNegatedSearchPreservesAnalysisMutationAndSerialization() throws Exception { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + Mockito.when(graph.searchPredicate("(beta)")).thenReturn("beta"::equals); + Traversal.Admin traversal = traversal( + __.V().has("body", P.not(Text.contains("(alpha)"))) + .limit(10).hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertFalse(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "(alpha)"))); + HasContainer clone = local.clone(); + @SuppressWarnings("unchecked") + P child = (P) ((NotP) local.getPredicate()).negate(); + child.setValue("(beta)"); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertFalse(local.test(searchVertex(graph, "beta"))); + Assert.assertFalse(clone.test(searchVertex(graph, "alpha"))); + HasContainer restored = roundTrip(local); + Assert.assertFalse(restored.test(searchVertex(graph, "beta"))); + Assert.assertTrue(restored.test(searchVertex(graph, "alpha"))); + + traversal = traversal(__.V().has("body", Text.contains("(alpha)") + .or(P.not(Text.contains("(alpha)")))) + .limit(10).hasLabel(P.neq("other")), graph); + source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "(alpha)"))); + } + + @Test + public void testNegatedLabelCollectionsResolveIdsAndNames() { + Id id = IdGenerator.of(12L); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, id, "person")); + for (Object candidate : new Object[]{id, id.asLong(), "person"}) { + P[] predicates = {P.not(P.within(candidate)), P.not(P.without(candidate)), + P.not(P.within(candidate).and(P.eq(candidate)))}; + boolean[] matches = {false, true, false}; + for (int i = 0; i < predicates.length; i++) { + Traversal.Admin traversal = __.V().has(T.label, predicates[i]).asAdmin(); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertEquals(matches[i], local.test(vertex)); + Assert.assertEquals(matches[i], local.clone().test(vertex)); + } + } + } + + private static HasContainer roundTrip(HasContainer container) throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { + output.writeObject(container); + } + try (ObjectInputStream input = new ObjectInputStream( + new ByteArrayInputStream(bytes.toByteArray()))) { + return (HasContainer) input.readObject(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testLocalSearchMatcherTracksPredicateMutation() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn("alpha"::equals); + Mockito.when(graph.searchPredicate("(beta)")).thenReturn("beta"::equals); + Traversal.Admin traversal = traversal( + __.V().has("body", Text.contains("(alpha)")) + .hasLabel(P.neq("other")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasContainer local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + Mockito.verify(graph, Mockito.times(1)).searchPredicate("(alpha)"); + HasContainer clone = local.clone(); + ((P) local.getPredicate()).setValue("(beta)"); + Assert.assertFalse(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "beta"))); + Assert.assertTrue(clone.test(searchVertex(graph, "alpha"))); + Assert.assertFalse(clone.test(searchVertex(graph, "beta"))); + + traversal = traversal(__.V().has("body", Text.contains("(alpha)").or(P.eq("gamma"))) + .hasLabel(P.neq("other")), graph); + source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + local = ((HasStep) source.getNextStep()).getHasContainers().get(0); + Assert.assertTrue(local.test(searchVertex(graph, "alpha"))); + ConnectiveP connective = (ConnectiveP) local.getPredicate(); + connective.getPredicates().get(0).setValue("(beta)"); + Assert.assertFalse(local.test(searchVertex(graph, "alpha"))); + Assert.assertTrue(local.test(searchVertex(graph, "beta"))); + Assert.assertTrue(local.test(searchVertex(graph, "gamma"))); + connective.or(P.eq("delta")); + Assert.assertTrue(local.test(searchVertex(graph, "delta"))); + } + + @SuppressWarnings("unchecked") + private static Vertex searchVertex(HugeGraph graph, String value) { + Vertex vertex = Mockito.mock(Vertex.class); + VertexProperty property = Mockito.mock(VertexProperty.class); + Mockito.when(vertex.graph()).thenReturn(graph); + Mockito.when(vertex.properties("body")) + .thenAnswer(ignored -> Collections.singletonList(property).iterator()); + Mockito.when(property.element()).thenReturn(vertex); + Mockito.when(property.value()).thenReturn(value); + return vertex; + } + + @Test + public void testLocalLabelIdWithoutGraphAndAfterClone() { + Id id = IdGenerator.of(12L); + Traversal.Admin traversal = __.V().has(T.label, id).limit(10) + .hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + TraversalUtil.convHasStep(null, (HasStep) source.getNextStep()); + HasContainer filter = ((HasStep) source.getNextStep()).getHasContainers().get(0); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, id, "v")); + Assert.assertTrue(filter.test(vertex)); + Assert.assertTrue(filter.clone().test(vertex)); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, IdGenerator.of(13L), "v")); + Assert.assertFalse(filter.test(vertex)); + Assert.assertFalse(filter.clone().test(vertex)); + } + + @Test + public void testSearchPredicateDefaultCompatibility() throws Exception { + Assert.assertTrue(HugeGraph.class.getMethod("searchPredicate", String.class).isDefault()); + HugeGraph graph = (HugeGraph) Proxy.newProxyInstance( + HugeGraph.class.getClassLoader(), new Class[]{HugeGraph.class}, + (proxy, method, args) -> MethodHandles.privateLookupIn( + method.getDeclaringClass(), MethodHandles.lookup()) + .unreflectSpecial(method, method.getDeclaringClass()) + .bindTo(proxy).invokeWithArguments(args)); + Assert.assertThrows(UnsupportedOperationException.class, () -> graph.searchPredicate("word")); + } + + @Test + public void testLocalContainsWithoutGraphAndAfterClone() { + Id key = IdGenerator.of(42L); + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("age")).thenReturn(propertyKey(42L, "age", DataType.INT)); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.graph()).thenReturn(graph); + Mockito.when(vertex.properties("key")).thenReturn(Collections.emptyIterator()); + Mockito.when(vertex.properties("value")).thenReturn(Collections.emptyIterator()); + BaseVertex baseVertex = Mockito.mock(BaseVertex.class); + Mockito.when(vertex.element()).thenReturn(baseVertex); + Mockito.when(baseVertex.sysprop(HugeKeys.PROPERTIES)) + .thenReturn(Collections.singletonMap(key, 20)); + for (GraphTraversal query : new GraphTraversal[]{ + __.V().hasKey("age").hasLabel(P.neq("other")), + __.V().hasValue(20).hasLabel(P.neq("other"))}) { + Traversal.Admin admin = query.asAdmin(); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + @SuppressWarnings("unchecked") + HasStep filter = (HasStep) source.getNextStep(); + @SuppressWarnings("unchecked") + Traverser.Admin traverser = Mockito.mock(Traverser.Admin.class); + Mockito.when(traverser.get()).thenReturn(vertex); + Mockito.when(traverser.bulk()).thenReturn(1L); + filter.addStart(traverser); + Assert.assertTrue(filter.hasNext()); + HasStep clone = filter.clone(); + clone.addStart(traverser); + Assert.assertTrue(clone.hasNext()); + } + } + + @Test + public void testLocalIdSurvivesPredicateConversionAcrossRange() { + for (P predicate : new P[]{P.eq("a").or(P.eq("b")), + P.within("a", "b").and(P.neq("b"))}) { + Traversal.Admin traversal = __.V().hasId(predicate) + .limit(10).hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + HasStep step = (HasStep) source.getNextStep(); + TraversalUtil.convHasStep(null, step); + HasContainer local = step.getHasContainers().get(0); + Vertex vertex = Mockito.mock(Vertex.class); + Mockito.when(vertex.id()).thenReturn(IdGenerator.of("a")); + Assert.assertTrue(local.test(vertex)); + Assert.assertTrue(local.clone().test(vertex)); + Mockito.when(vertex.id()).thenReturn(IdGenerator.of("z")); + Assert.assertFalse(local.test(vertex)); + } + } + + @Test + public void testUnsafeLabelKeepsPointLookupPlan() { + for (GraphTraversal query : new GraphTraversal[]{ + __.V().hasId(1).limit(10).hasLabel(P.neq("other")), + EmptyGraph.instance().traversal().E().hasId(1).limit(10).hasLabel(P.neq("other")), + __.V().hasId(P.within(1, 2)).hasLabel(P.neq("other"))}) { + Traversal.Admin admin = query.asAdmin(); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(source.getIds().length > 0); + Assert.assertEquals(1, source.getIds()[0]); + Assert.assertFalse(hasStepExists(admin, T.id.getAccessor())); + } + Traversal.Admin admin = __.V(1, 2).hasId(2) + .hasLabel(P.neq("other")).asAdmin(); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertEquals(2, source.getIds().length); + Assert.assertTrue(hasStepExists(admin, T.id.getAccessor())); + + admin = __.V().hasId(P.neq(1)).hasLabel(P.neq("other")).asAdmin(); + source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertEquals(0, source.getIds().length); + Assert.assertTrue(hasStepExists(admin, T.id.getAccessor())); + } + + @Test + public void testLabelAfterElementChangeKeepsSourceIndexPlan() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").in().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").both().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").outE().hasLabel(P.neq("knows")), + __.V().has("city", "Beijing").outE().hasLabel(P.neq("knows")).dedup(), + __.V().has("city", "Beijing").outE().hasLabel(P.neq("knows")) + .order().by("weight"), + __.V().has("city", "Beijing").inE().hasLabel(P.neq("knows")), + __.V().has("city", "Beijing").bothE().hasLabel(P.neq("knows")), + __.V().has("city", "Beijing").out().in().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").both().both().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).count(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).dedup(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .order().by("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .valueMap("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .elementMap("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).fold(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .groupCount(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")) + .project("n").by("name"), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).id(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).label(), + __.V().has("city", "Beijing").out().hasLabel(P.neq("author")).values("age").sum(), + __.V().has("city", "Beijing").out().where(__.not(__.hasLabel("author"))).count(), + __.V().has("city", "Beijing").out().where(__.not(__.hasLabel("author"))), + __.V().has("city", "Beijing").properties().hasLabel(P.neq("author"))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(hasContainer(source, "city")); + Assert.assertFalse(hasStepExists(admin, "city")); + } + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").as("a").out().select("a").hasLabel(P.neq("author")), + __.V().has("city", "Beijing").out().path().unfold().hasLabel(P.neq("author")), + __.V().has("city", "Beijing").as("a").out().hasLabel(P.neq("author")) + .dedup().by(__.select("a")), + __.V().has("city", "Beijing").as("a").out().hasLabel(P.neq("author")) + .order().by(__.select("a")), + __.V().has("city", "Beijing").as("a").out().hasLabel(P.neq("author")) + .project("n").by(__.select("a")), + __.V().has("city", "Beijing").out().filter(__.select("a").hasLabel(P.neq("author")))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + } + + @Test + public void testChildElementChangeKeepsSourceIndexPlan() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").where(__.out().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").where(__.out().in().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").where(__.both().both().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").where(__.outE().hasLabel(P.neq("knows"))), + __.V().has("city", "Beijing").where(__.inE().hasLabel(P.neq("knows"))), + __.V().has("city", "Beijing").where(__.bothE().hasLabel(P.neq("knows"))), + __.V().has("city", "Beijing").filter(__.out().hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").not(__.out().hasLabel("author"))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(hasContainer(source, "city")); + Assert.assertFalse(hasStepExists(admin, "city")); + } + for (GraphTraversal query : new GraphTraversal[]{ + __.V().has("city", "Beijing").where(__.hasLabel(P.neq("author"))), + __.V().has("city", "Beijing").as("a") + .where(__.out().select("a").hasLabel(P.neq("author"))), + __.V().has("city", "Beijing") + .where(__.out().path().unfold().hasLabel(P.neq("author")))}) { + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + } + + @Test + public void testStandardSuffixesKeepRootAndChildSourceIndexPlan() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal suffix : new GraphTraversal[]{ + __.tail(1), __.values("age").is(P.gt(1)), __.fold().unfold(), + __.constant(1), __.sample(2), __.sample(2).by("name"), + __.group().by(T.label), __.group().by(T.label).by(__.values("age").fold()), + __.aggregate("x"), __.aggregate("x").by("name"), + __.group("x").by(T.label), + __.groupCount("x").by(T.label), __.simplePath(), __.cyclicPath(), + __.simplePath().by("name"), __.coin(0.5d), __.timeLimit(100), + __.fold().tail(Scope.local, 1), __.fold().limit(Scope.local, 1), + __.fold().order(Scope.local), __.fold().dedup(Scope.local), + __.fold().sample(Scope.local, 1), __.fold().count(Scope.local), + __.values("age").fold().sum(Scope.local), + __.values("age").fold().min(Scope.local), + __.values("age").fold().max(Scope.local), + __.values("age").fold().mean(Scope.local), __.fold().index(), + __.sideEffect(__.values("name")), __.identity().profile("metrics")}) { + for (boolean child : new boolean[]{false, true}) { + GraphTraversal chain = __.out().hasLabel(P.neq("author")); + for (Step step : suffix.asAdmin().getSteps()) { + chain.asAdmin().addStep(step.clone()); + } + GraphTraversal query = __.V().has("city", "Beijing"); + if (child) { + query.where(chain); + } else { + for (Step step : chain.asAdmin().getSteps()) { + query.asAdmin().addStep(step.clone()); + } + } + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertTrue(query.toString(), hasContainer(source, "city")); + Assert.assertFalse(query.toString(), hasStepExists(admin, "city")); + } + } + } + + @Test + public void testHistoryAndOpaqueSuffixesKeepSourceFilterLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (GraphTraversal suffix : new GraphTraversal[]{ + __.group().by(__.select("a")), __.group().by(T.label).by(__.select("a")), + __.aggregate("x").by(__.select("a")), + __.sample(2).by(__.select("a")), __.simplePath().by(__.select("a")), + __.group("x").by(__.select("a")), __.groupCount("x").by(__.select("a")), + __.dedup("a"), __.select("a"), __.path().unfold(), __.tree(), + __.cap("x").unfold(), __.sack(), __.repeat(__.select("a")).times(1), + __.where(P.eq("a")), __.match(__.as("a").out().as("b")), + __.map(t -> t.path().get("a")), __.filter(t -> true), + __.fold(0, (a, b) -> a), __.sideEffect(t -> { })}) { + GraphTraversal query = __.V().has("city", "Beijing").as("a") + .out().hasLabel(P.neq("author")); + for (Step step : suffix.asAdmin().getSteps()) { + query.asAdmin().addStep(step.clone()); + } + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(query.toString(), hasContainer(source, "city")); + Assert.assertTrue(query.toString(), hasStepExists(admin, "city")); + } + } + + @Test + public void testUnknownHasStepSubclassKeepsSourceFilterLocal() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + Traversal.Admin admin = traversal(__.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")), graph); + admin.addStep(new HasStep<>(admin, new HasContainer("name", P.eq("x"))) { }); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + + @Test + public void testFilterResultUsesSchemaLabelRepresentations() { + Id id = IdGenerator.of(7L); + HugeVertex vertex = Mockito.mock(HugeVertex.class); + Mockito.when(vertex.schemaLabel()).thenReturn(new VertexLabel(null, id, "person")); + for (P predicate : new P[]{P.eq("person"), P.eq(id), P.eq(id.asLong()), + P.within(id, "person"), P.neq("other")}) { + Assert.assertTrue(TraversalUtil.filterResult( + Collections.singletonList(new HasContainer(T.label.getAccessor(), predicate)), + Collections.singletonList(vertex).iterator()).hasNext()); + } + Assert.assertFalse(TraversalUtil.filterResult(Collections.singletonList( + new HasContainer(T.label.getAccessor(), P.eq("other"))), + Collections.singletonList(vertex).iterator()).hasNext()); + } + + @Test + public void testEdgeEndpointsKeepSourceLabelFallback() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")) + .thenReturn(propertyKey(2L, "city", DataType.TEXT)); + for (boolean child : new boolean[]{false, true}) { + for (GraphTraversal suffix : new GraphTraversal[]{ + __.outE().outV(), __.inE().inV(), __.bothE().bothV(), + __.outE().inV(), __.inE().outV(), + __.outE().barrier().outV(), + __.outE().filter(__.outV().hasLabel(P.neq("author")))}) { + suffix.hasLabel(P.neq("author")); + GraphTraversal query = __.V().has("city", "Beijing"); + if (child) { + query.where(suffix); + } else { + for (Step step : new ArrayList( + suffix.asAdmin().getSteps())) { + suffix.asAdmin().removeStep(step); + query.asAdmin().addStep(step); + } + } + Traversal.Admin admin = traversal(query, graph); + HugeGraphStep source = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(source, admin); + Assert.assertFalse(hasContainer(source, "city")); + Assert.assertTrue(hasStepExists(admin, "city")); + } + } + } + + @Test + public void testChildSourceKeepsPropertyBeforeParentUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + GraphTraversal child = __.V().has("city", "Beijing"); + traversal(__.inject(1).union(child).hasLabel(P.neq("author")), graph); + Traversal.Admin admin = traversal(child, graph); + HugeGraphStep graphStep = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(graphStep, admin); + Assert.assertTrue(graphStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(admin, "city")); + + child = __.out().has("city", "Beijing"); + traversal(__.V().local(child).hasLabel(P.neq("author")), graph); + admin = traversal(child, graph); + HugeVertexStep vertexStep = replaceVertexStep(admin); + TraversalUtil.extractHasContainer(vertexStep, admin); + Assert.assertTrue(vertexStep.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(admin, "city")); + } + + @Test + public void testChildSourceStillExtractsWithoutDownstreamUnsafeLabel() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + GraphTraversal child = __.V().has("city", "Beijing"); + traversal(__.inject(1).union(child).hasLabel("person"), graph); + Traversal.Admin admin = traversal(child, graph); + HugeGraphStep graphStep = replaceGraphStep(admin); + TraversalUtil.extractHasContainer(graphStep, admin); + Assert.assertTrue(hasContainer(graphStep, "city")); + Assert.assertFalse(hasStepExists(admin, "city")); + } + + @Test + public void testNotContextOnlyBlocksLabelDependentPushdown() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + Mockito.when(graph.propertyKey("city")).thenReturn(propertyKey(2L, "city", DataType.TEXT)); + Traversal.Admin traversal = traversal( + __.V().has("city", "Beijing").not(__.hasLabel("author")), graph); + HugeGraphStep source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertTrue(source.getHasContainers().isEmpty()); + Assert.assertTrue(hasStepExists(traversal, "city")); + + traversal = traversal(__.V().has("city", "Beijing").not(__.has("name", "Tom")), graph); + source = replaceGraphStep(traversal); + TraversalUtil.extractHasContainer(source, traversal); + Assert.assertTrue(hasContainer(source, "city")); + Assert.assertFalse(hasStepExists(traversal, "city")); + } + @Test public void testConnectiveLabelStepStrategyApplyPost() { Set> post = @@ -608,12 +1454,17 @@ private static boolean hasContainer(HasContainerHolder step, } 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/auth/HugeGraphAuthProxyTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java index e5b8494945..13216d2164 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/auth/HugeGraphAuthProxyTest.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeGraph; @@ -129,6 +130,30 @@ public void testUsernameWithNullContext() { Assert.assertEquals("anonymous", username); } + @Test + public void testSearchPredicateDoesNotRequireAdminConfigAccess() { + HugeGraph graph = Mockito.mock(HugeGraph.class); + HugeConfig config = Mockito.mock(HugeConfig.class); + Mockito.when(graph.name()).thenReturn("hugegraph"); + Mockito.when(graph.graphSpace()).thenReturn("DEFAULT"); + Mockito.when(graph.spaceGraphName()).thenReturn("hugegraph"); + Mockito.when(graph.configuration()).thenReturn(config); + Mockito.when(graph.authManager()).thenReturn(Mockito.mock(AuthManager.class)); + Mockito.when(graph.taskScheduler()).thenReturn(Mockito.mock(TaskScheduler.class)); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_EXPIRE)).thenReturn(3600L); + Mockito.when(config.get(AuthOptions.AUTH_CACHE_CAPACITY)).thenReturn(100L); + Mockito.when(config.get(AuthOptions.AUTH_AUDIT_LOG_RATE)).thenReturn(1000D); + Predicate matcher = "alpha"::equals; + Mockito.when(graph.searchPredicate("(alpha)")).thenReturn(matcher); + HugeGraphAuthProxy proxy = new HugeGraphAuthProxy(graph); + setContext(new HugeGraphAuthProxy.Context(new HugeAuthenticator.User( + "reader", RolePermission.all("hugegraph")))); + Mockito.clearInvocations(graph); + + Assert.assertSame(matcher, proxy.searchPredicate("(alpha)")); + Mockito.verify(graph, Mockito.never()).configuration(); + } + @Test public void testUsernameWithValidContext() { // Create a user with a specific username diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java index c7bbfd874f..4a529bc0e3 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/cache/RamTableTest.java @@ -17,25 +17,35 @@ package org.apache.hugegraph.unit.cache; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.List; -import org.apache.hugegraph.exception.HugeException; import org.apache.hugegraph.HugeFactory; import org.apache.hugegraph.HugeGraph; -import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.store.ram.RamTable; -import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.exception.HugeException; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.struct.schema.EdgeLabel; import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.unit.FakeObjects; import org.junit.After; import org.junit.Before; import org.junit.Test; +import com.google.common.collect.ImmutableList; + public class RamTableTest { // max value is 4 billion @@ -81,6 +91,129 @@ private HugeGraph graph() { return this.graph; } + @Test + public void testMatchedLabelCandidateContract() { + RamTable table = new RamTable(this.graph(), 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, 1); + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of(IdGenerator.of(1)))); + Assert.assertTrue(table.matched(query)); + + query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(IdGenerator.of(1), IdGenerator.of(2)))); + Assert.assertTrue(table.matched(query)); + query.eq(HugeKeys.LABEL, IdGenerator.of(3)); + Assert.assertFalse(table.matched(query)); + } + + @Test + public void testQueryBranchRejectsUnresolvedLabels() { + RamTable table = new RamTable(this.graph(), 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, 1); + for (Condition[] labels : new Condition[][]{ + {Condition.in(HugeKeys.LABEL, ImmutableList.of())}, + {Condition.eq(HugeKeys.LABEL, IdGenerator.of(1)), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(2))}, + {Condition.in(HugeKeys.LABEL, + ImmutableList.of(IdGenerator.of(1), IdGenerator.of(2)))}}) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + for (Condition label : labels) { + query.query(label); + } + // Bypass matched()/flatten(): the branch itself must fail closed, + // independently of Java assertions and the caller's invariants. + Assert.assertThrows(IllegalStateException.class, () -> + Whitebox.invoke(RamTable.class, "query", table, query)); + } + } + + @Test + public void testQueryBranchWildcardAndSingleLabel() { + HugeGraph graph = this.graph(); + int el1 = (int) graph.edgeLabel("el1").id().asLong(); + int el2 = (int) graph.edgeLabel("el2").id().asLong(); + RamTable table = new RamTable(graph, 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, el1); + table.addEdge(false, 1, 3, Directions.OUT, el2); + for (boolean wildcard : new boolean[]{true, false}) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + if (!wildcard) { + query.eq(HugeKeys.LABEL, IdGenerator.of(el1)); + } + Iterator edges = Whitebox.invoke(RamTable.class, "query", table, query); + List targets = new ArrayList<>(); + edges.forEachRemaining(edge -> targets.add(edge.id().otherVertexId().asLong())); + Collections.sort(targets); + Assert.assertEquals(wildcard ? ImmutableList.of(2L, 3L) : ImmutableList.of(2L), targets); + } + } + + @Test + public void testQueryByMultipleLabels() { + HugeGraph graph = this.graph(); + int el1 = (int) graph.edgeLabel("el1").id().asLong(); + int el2 = (int) graph.edgeLabel("el2").id().asLong(); + int other = (int) graph.schema().edgeLabel("other") + .sourceLabel("vl1").targetLabel("vl1").create().id().asLong(); + RamTable table = new RamTable(graph, 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, el1); + table.addEdge(false, 1, 3, Directions.IN, el2); + table.addEdge(false, 1, 4, Directions.OUT, other); + for (Directions direction : Directions.values()) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + if (direction == Directions.BOTH) { + query.query(Condition.or(Condition.eq(HugeKeys.DIRECTION, Directions.OUT), + Condition.eq(HugeKeys.DIRECTION, Directions.IN))); + } else { + query.eq(HugeKeys.DIRECTION, direction); + } + query.query(Condition.in(HugeKeys.LABEL, ImmutableList.of( + IdGenerator.of(el1), IdGenerator.of(el2), IdGenerator.of(el1)))); + Assert.assertTrue(table.matched(query)); + List targets = new ArrayList<>(); + table.query(query).forEachRemaining(edge -> { + Assert.assertEquals(1L, edge.id().ownerVertexId().asLong()); + Assert.assertEquals(edge.direction() == Directions.OUT ? "el1" : "el2", + edge.label()); + targets.add(edge.id().otherVertexId().asLong()); + }); + Collections.sort(targets); + Assert.assertEquals(direction == Directions.BOTH ? ImmutableList.of(2L, 3L) : + ImmutableList.of(direction == Directions.OUT ? 2L : 3L), targets); + } + } + + @Test + public void testMatchedRejectsUnsafeLabelCandidates() { + RamTable table = new RamTable(this.graph(), 10, 20); + table.addEdge(true, 1, 2, Directions.OUT, 1); + for (Condition label : new Condition[]{ + Condition.in(HugeKeys.LABEL, ImmutableList.of()), + Condition.neq(HugeKeys.LABEL, IdGenerator.of(1)), + Condition.in(HugeKeys.LABEL, ImmutableList.of("el1", "el2")), + Condition.eq(HugeKeys.LABEL, IdGenerator.ZERO), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(-1)), + Condition.eq(HugeKeys.LABEL, IdGenerator.of(1L << 32))}) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + query.query(label); + Assert.assertFalse(table.matched(query)); + } + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + Assert.assertTrue(table.matched(query)); + query.query(Condition.in(HugeKeys.LABEL, + ImmutableList.of(IdGenerator.of(1), IdGenerator.of(2)))); + query.query(Condition.neq(HugeKeys.LABEL, IdGenerator.of(1))); + Assert.assertFalse(table.matched(query)); + } + @Test public void testAddAndQuery() throws Exception { HugeGraph graph = this.graph(); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/core/QueryTest.java index 622535cbc0..49fc1b1810 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 @@ -17,6 +17,9 @@ package org.apache.hugegraph.unit.core; +import java.lang.reflect.Modifier; +import java.util.List; + import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.query.Aggregate.AggregateFunc; @@ -25,8 +28,8 @@ import org.apache.hugegraph.query.IdPrefixQuery; import org.apache.hugegraph.query.IdQuery; import org.apache.hugegraph.query.IdRangeQuery; -import org.apache.hugegraph.query.Query; import org.apache.hugegraph.query.Query.Order; +import org.apache.hugegraph.query.Query; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.HugeKeys; @@ -38,6 +41,25 @@ public class QueryTest { + @Test + public void testContainsConditionOverloadVisibility() throws Exception { + Assert.assertThrows(NoSuchMethodException.class, + () -> ConditionQuery.class.getMethod("containsCondition", Object.class)); + Assert.assertTrue(Modifier.isPrivate(ConditionQuery.class + .getDeclaredMethod("containsCondition", Object.class).getModifiers())); + Assert.assertTrue(Modifier.isPublic(ConditionQuery.class + .getMethod("containsCondition", HugeKeys.class).getModifiers())); + Assert.assertTrue(Modifier.isPublic(ConditionQuery.class + .getMethod("containsCondition", Condition.RelationType.class).getModifiers())); + + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, IdGenerator.of(1L)); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsCondition(HugeKeys.NAME)); + Assert.assertTrue(query.containsCondition(Condition.RelationType.EQ)); + Assert.assertFalse(query.containsCondition(Condition.RelationType.IN)); + } + @Test public void testOrderBy() { Query query = new Query(HugeType.VERTEX); @@ -48,6 +70,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 +93,67 @@ 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 testConditionWithSingletonAndDuplicateInValues() { + Id label = IdGenerator.of(1); + for (List values : ImmutableList.of(ImmutableList.of(label), + ImmutableList.of(label, label))) { + ConditionQuery query = new ConditionQuery(HugeType.EDGE); + query.query(Condition.in(HugeKeys.LABEL, values)); + Assert.assertEquals(values, query.condition(HugeKeys.LABEL)); + Assert.assertEquals(ImmutableSet.of(label), query.conditionValues(HugeKeys.LABEL)); + Assert.assertEquals(label, query.conditionValue(HugeKeys.LABEL)); + Assert.assertEquals(label, query.singleConditionValueOrNull(HugeKeys.LABEL)); + } + } + @Test public void testConditionWithConflictingEqAndIn() { Id label1 = IdGenerator.of(1); @@ -73,6 +166,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 +205,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'", diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java index 92fa5976bc..853c981aae 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/unit/serializer/BinarySerializerTest.java @@ -17,47 +17,77 @@ package org.apache.hugegraph.unit.serializer; -import java.util.Arrays; -import java.util.Date; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Date; import java.util.Properties; import java.util.Set; -import org.apache.hugegraph.id.EdgeId; -import org.apache.hugegraph.id.Id; -import org.apache.hugegraph.id.IdGenerator; import org.apache.hugegraph.backend.BackendColumn; +import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.serializer.BinaryBackendEntry; -import org.apache.hugegraph.query.ConditionQuery; -import org.apache.hugegraph.struct.schema.IndexLabel; -import org.apache.hugegraph.structure.Index; -import org.apache.hugegraph.structure.Index.IdWithExpiredTime; -import org.apache.hugegraph.type.HugeType; -import org.apache.hugegraph.type.define.Directions; -import org.apache.hugegraph.type.define.IdStrategy; -import org.apache.hugegraph.type.define.IndexType; -import org.apache.hugegraph.serializer.BytesBuffer; -import org.apache.hugegraph.type.define.HugeKeys; -import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.backend.serializer.BinarySerializer; +import org.apache.hugegraph.backend.serializer.TextSerializer; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.config.HugeConfig; +import org.apache.hugegraph.id.EdgeId; +import org.apache.hugegraph.id.Id; +import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; +import org.apache.hugegraph.query.IdPrefixQuery; +import org.apache.hugegraph.serializer.BytesBuffer; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.struct.schema.Userdata; import org.apache.hugegraph.structure.HugeEdge; import org.apache.hugegraph.structure.HugeVertex; +import org.apache.hugegraph.structure.Index.IdWithExpiredTime; +import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; +import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; +import org.apache.hugegraph.type.define.Directions; +import org.apache.hugegraph.type.define.HugeKeys; +import org.apache.hugegraph.type.define.IdStrategy; +import org.apache.hugegraph.type.define.IndexType; import org.apache.hugegraph.unit.BaseUnitTest; import org.apache.hugegraph.unit.FakeObjects; +import org.apache.hugegraph.util.Bytes; import org.apache.hugegraph.util.DateUtil; import org.junit.Test; import org.mockito.Mockito; public class BinarySerializerTest extends BaseUnitTest { + @Test + public void testEdgeQueryLabelCandidateContract() { + for (AbstractSerializer serializer : Arrays.asList( + new BinarySerializer(FakeObjects.newConfig()), + new TextSerializer(FakeObjects.newConfig()))) { + ConditionQuery eq = new ConditionQuery(HugeType.EDGE); + eq.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + eq.eq(HugeKeys.DIRECTION, Directions.OUT); + eq.eq(HugeKeys.LABEL, IdGenerator.of(2)); + ConditionQuery in = new ConditionQuery(HugeType.EDGE); + in.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + in.eq(HugeKeys.DIRECTION, Directions.OUT); + in.query(Condition.in(HugeKeys.LABEL, Arrays.asList(IdGenerator.of(2)))); + Assert.assertEquals(((IdPrefixQuery) serializer.writeQuery(eq)).prefix(), + ((IdPrefixQuery) serializer.writeQuery(in)).prefix()); + + ConditionQuery multiple = new ConditionQuery(HugeType.EDGE); + multiple.eq(HugeKeys.OWNER_VERTEX, IdGenerator.of(1)); + multiple.eq(HugeKeys.DIRECTION, Directions.OUT); + multiple.query(Condition.in(HugeKeys.LABEL, + Arrays.asList(IdGenerator.of(2), IdGenerator.of(3)))); + Assert.assertThrows(IllegalStateException.class, () -> serializer.writeQuery(multiple)); + } + } + @Test public void testLegacyStoredLabelExpiryReadEliminateAndAppendKeys() throws IOException { Properties core = labelFixtures("core-rows.properties"); diff --git a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java index 44bc00e824..8fca84212c 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/query/ConditionQuery.java @@ -376,24 +376,32 @@ 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 the single EQ value if there is exactly one top-level EQ + * relation and no top-level IN relation
  • + *
  • returns the raw IN list, including an empty list, if there is exactly + * one top-level IN relation and no top-level EQ relation
  • + *
  • returns {@code null} if several top-level EQ/IN relations resolve to + * an empty set
  • + *
  • returns the single value if several relations resolve to one value
  • + *
  • throws if multiple values remain after resolving several relations
  • + *
+ * + * 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; } @@ -408,29 +416,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; @@ -443,20 +430,151 @@ public T condition(Object key) { return value; } + /** + * Returns whether there is any top-level relation for the specified key. + */ + private 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-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java index 0929a3b2f0..72c5ebc6a7 100644 --- a/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java +++ b/hugegraph-struct/src/main/java/org/apache/hugegraph/structure/builder/IndexBuilder.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import org.apache.commons.lang3.StringUtils; import org.apache.hugegraph.HugeGraphSupplier; @@ -42,9 +43,10 @@ import org.apache.hugegraph.structure.BaseProperty; import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.structure.Index; +import org.apache.hugegraph.util.CollectionUtil; import org.apache.hugegraph.util.E; -import org.apache.hugegraph.util.Log; import org.apache.hugegraph.util.InsertionOrderUtil; +import org.apache.hugegraph.util.Log; import org.apache.hugegraph.util.NumericUtil; import org.slf4j.Logger; @@ -314,13 +316,23 @@ private static boolean isCollectionIndex(List propValues) { } public Set segmentWords(String text) { + return segmentWords(this.textAnalyzer, text); + } + + public static Predicate searchPredicate(Analyzer analyzer, String text) { + Set words = segmentWords(analyzer, text); + return value -> CollectionUtil.hasIntersection(words, + segmentWords(analyzer, propertyValueToString(value))); + } + + private static Set segmentWords(Analyzer analyzer, String text) { if (text.startsWith(START_SYMBOL) && text.endsWith(END_SYMBOL)) { String words = text.substring(1, text.length() - 1); return words.contains(WORD_DELIMITER) ? ImmutableSet.copyOf(StringUtils.split(words, WORD_DELIMITER)) : ImmutableSet.of(words); } Set segments = InsertionOrderUtil.newSet(); - segments.addAll(this.textAnalyzer.segment(text)); + segments.addAll(analyzer.segment(text)); segments.add(text); segments.removeAll(ConditionQuery.IGNORE_SYM_SET); return segments; diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.java index f73e6f416f..f01bdb2d3c 100644 --- a/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.java +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/query/SharedQueryTest.java @@ -21,11 +21,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import org.apache.hugegraph.query.Aggregate.AggregateFunc; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; +import org.apache.hugegraph.query.Aggregate.AggregateFunc; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.type.HugeType; @@ -67,6 +68,41 @@ public void testContradictoryIntersectionsStayEmpty() { Assert.assertNull(emptyFirst.condition(HugeKeys.LABEL)); } + @Test + public void testExplicitConditionResolutionPreservesLegacyInList() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertTrue(query.conditionValues(HugeKeys.LABEL).isEmpty()); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + + List labels = Arrays.asList(1, 2, 2); + query.query(Condition.in(HugeKeys.LABEL, labels)); + Assert.assertSame(labels, query.condition(HugeKeys.LABEL)); + Assert.assertEquals(new LinkedHashSet<>(labels), query.conditionValues(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + Assert.assertThrows(IllegalStateException.class, () -> query.conditionValue(HugeKeys.LABEL)); + + query.eq(HugeKeys.LABEL, 2); + Assert.assertEquals(Integer.valueOf(2), query.conditionValue(HugeKeys.LABEL)); + Assert.assertEquals(Integer.valueOf(2), query.singleConditionValueOrNull(HugeKeys.LABEL)); + query.eq(HugeKeys.LABEL, 3); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertTrue(query.conditionValues(HugeKeys.LABEL).isEmpty()); + Assert.assertNull(query.conditionValue(HugeKeys.LABEL)); + } + + @Test + public void testEmptyInAndNegativeConditionAreDistinct() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.neq(HugeKeys.LABEL, 1); + Assert.assertTrue(query.containsCondition(HugeKeys.LABEL)); + Assert.assertFalse(query.containsConditionValues(HugeKeys.LABEL)); + query.query(Condition.in(HugeKeys.LABEL, Collections.emptyList())); + Assert.assertTrue(query.containsConditionValues(HugeKeys.LABEL)); + Assert.assertEquals(Collections.emptyList(), query.condition(HugeKeys.LABEL)); + Assert.assertNull(query.singleConditionValueOrNull(HugeKeys.LABEL)); + } + @Test public void testSingleIdAndCopy() { Id id = IdGenerator.of(123L); diff --git a/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.java b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.java index 775c261cee..d902019b29 100644 --- a/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.java +++ b/hugegraph-struct/src/test/java/org/apache/hugegraph/structure/builder/IndexBuilderTest.java @@ -20,31 +20,32 @@ import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.HashMap; -import java.util.Map; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.apache.hugegraph.HugeGraphSupplier; +import org.apache.hugegraph.analyzer.Analyzer; +import org.apache.hugegraph.id.EdgeId; import org.apache.hugegraph.id.Id; import org.apache.hugegraph.id.IdGenerator; -import org.apache.hugegraph.id.EdgeId; import org.apache.hugegraph.query.MatchedIndex; -import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.EdgeLabel; +import org.apache.hugegraph.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.PropertyKey; import org.apache.hugegraph.struct.schema.VertexLabel; -import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.structure.BaseEdge; +import org.apache.hugegraph.structure.BaseVertex; import org.apache.hugegraph.structure.Index; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.Cardinality; import org.apache.hugegraph.type.define.DataType; -import org.apache.hugegraph.type.define.IndexType; -import org.apache.hugegraph.type.define.WriteType; import org.apache.hugegraph.type.define.Directions; import org.apache.hugegraph.type.define.EdgeLabelType; +import org.apache.hugegraph.type.define.IndexType; +import org.apache.hugegraph.type.define.WriteType; import org.junit.Assert; import org.junit.Test; @@ -83,6 +84,16 @@ public void testSearchExpansionForCollectionIncludesJoinedText() { Assert.assertEquals("alpha beta", IndexBuilder.propertyValueToString(List.of("alpha", "beta"))); } + @Test + public void testSearchPredicateUsesSharedAnalysisForScalarAndCollection() { + Analyzer analyzer = text -> Set.of(text.split(" ")); + Assert.assertTrue(IndexBuilder.searchPredicate(analyzer, "(alpha)").test("alpha beta")); + Assert.assertTrue(IndexBuilder.searchPredicate(analyzer, "(alpha|gamma)").test(List.of("beta", "gamma"))); + Assert.assertTrue(IndexBuilder.searchPredicate(analyzer, "alpha delta").test("alpha beta")); + Assert.assertFalse(IndexBuilder.searchPredicate(analyzer, "(gamma)").test("alpha beta")); + Assert.assertFalse(IndexBuilder.searchPredicate(analyzer, "(alpha)").test("alphabet")); + } + @Test public void testNullablePrefixAndUniqueNullExpansion() { HugeGraphSupplier graph = supplier();