From ae90ad237703ba875d8627c92e521ab3a8a4cf0b Mon Sep 17 00:00:00 2001 From: contrueCT Date: Sun, 27 Sep 2026 15:55:05 +0800 Subject: [PATCH 1/8] fix(core): reject incomplete source index coverage --- .../backend/tx/GraphIndexTransaction.java | 57 ++- .../apache/hugegraph/core/CoreTestSuite.java | 1 + .../apache/hugegraph/core/EdgeCoreTest.java | 260 ++++++------ .../core/SourceIndexCoverageTest.java | 362 ++++++++++++++++ .../apache/hugegraph/core/VertexCoreTest.java | 401 +++++++++--------- 5 files changed, 755 insertions(+), 326 deletions(-) create mode 100644 hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 6faace9671..d84917470a 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 @@ -480,7 +480,7 @@ private IdHolderList queryByUserprop(ConditionQuery query) { } } } - Set indexes = this.collectMatchedIndexes(query); + Set indexes = this.collectMatchedIndexes(query, true); if (indexes.isEmpty()) { Id label = query.condition(HugeKeys.LABEL); throw noIndexException(this.graph(), query, label); @@ -492,8 +492,8 @@ private IdHolderList queryByUserprop(ConditionQuery query) { return IdHolderList.empty(paging); } - // Do index query - IdHolderList holders = new IdHolderList(paging); + // Validate the whole plan before opening any backend iterators. A later + // label's unavailable index must not leave earlier iterators unowned. for (MatchedIndex index : indexes) { for (IndexLabel il : index.indexLabels()) { validateIndexLabel(il); @@ -501,7 +501,11 @@ private IdHolderList queryByUserprop(ConditionQuery query) { if (paging && index.indexLabels().size() > 1) { throw new NotSupportException("joint index query in paging"); } + } + // Do index query + IdHolderList holders = new IdHolderList(paging); + for (MatchedIndex index : indexes) { if (index.containsSearchIndex()) { // Do search-index query holders.addAll(this.doSearchIndex(query, index)); @@ -765,8 +769,13 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, } } - @Watched(prefix = "index") private Set collectMatchedIndexes(ConditionQuery query) { + return this.collectMatchedIndexes(query, false); + } + + @Watched(prefix = "index") + private Set collectMatchedIndexes(ConditionQuery query, + boolean requireCompleteCoverage) { ISchemaTransaction schema = this.params().schemaTransaction(); Id label = query.condition(HugeKeys.LABEL); @@ -805,9 +814,49 @@ private Set collectMatchedIndexes(ConditionQuery query) { matchedIndexes.add(index); } } + if (requireCompleteCoverage && !matchedIndexes.isEmpty()) { + this.checkIndexCoverage(query, schemaLabels, matchedIndexes); + } return matchedIndexes; } + private void checkIndexCoverage(ConditionQuery query, + List labels, + Set indexes) { + // Check at execution, before creating any lazy index iterator. Cleanup + // callers only need matching indexes and must not require full coverage. + Set requiredProperties = query.userpropKeys(); + if (this.graph().readMode().showOlap()) { + // OLAP properties are shared rather than declared on every label. + requiredProperties.removeIf(id -> this.graph().propertyKey(id).olap()); + } + Set covered = new HashSet<>(); + for (MatchedIndex index : indexes) { + covered.add(index.schemaLabel().id()); + } + for (SchemaLabel label : labels) { + if (covered.contains(label.id()) || + !label.properties().containsAll(requiredProperties)) { + continue; + } + if (label instanceof EdgeLabel) { + EdgeLabel edgeLabel = (EdgeLabel) label; + // Base labels have no direct edges; their property indexes + // include edges of their sub-labels (see updateEdgeIndex()). + if (edgeLabel.isFather() || + edgeLabel.hasFather() && covered.contains(edgeLabel.fatherId())) { + continue; + } + } + // Authorization filters elements after the backend query. Do not + // disclose an implicitly discovered schema label in this error. + throw new NoIndexException("Incomplete index coverage for properties %s; " + + "specify a label or create compatible indexes " + + "for every candidate label", + this.graph().mapPkId2Name(query.userpropKeys())); + } + } + /** * Collect matched IndexLabel(s) in a SchemaLabel for a query * diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java index f18d656c08..1a4e64fe40 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/CoreTestSuite.java @@ -39,6 +39,7 @@ EdgeLabelCoreTest.class, IndexLabelCoreTest.class, VertexCoreTest.class, + SourceIndexCoverageTest.class, EdgeCoreTest.class, CountStrategyCoreTest.class, PrimaryKeyStrategyCoreTest.class, 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 9ea6b0e919..b9ba7674b7 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 @@ -755,7 +755,7 @@ public void testAddEdgeWithSecondaryIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", "library of school"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -767,7 +767,7 @@ public void testAddEdgeWithSecondaryIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); } @@ -797,27 +797,27 @@ public void testAddEdgeWithRangeIndexAndTtl() { "date", "2019-12-23 16:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -829,22 +829,22 @@ public void testAddEdgeWithRangeIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -878,31 +878,31 @@ public void testAddEdgeWithShardIndexAndTtl() { "date", "2019-12-23 14:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E().has("place", "home"); + Iterator edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge6, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -914,26 +914,26 @@ public void testAddEdgeWithShardIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -951,7 +951,7 @@ public void testAddEdgeWithSearchIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", Text.contains("library")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -963,7 +963,7 @@ public void testAddEdgeWithSearchIndexAndTtl() { // Ignore } - edges = graph().traversal().E().has("place", Text.contains("library")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("library")); Assert.assertFalse(edges.hasNext()); } @@ -1010,7 +1010,7 @@ public void testOverrideEdgeWithSecondaryIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", "library of school"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -1028,10 +1028,10 @@ public void testOverrideEdgeWithSecondaryIndexAndTtl() { graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); graph().tx().commit(); @@ -1043,9 +1043,9 @@ public void testOverrideEdgeWithSecondaryIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); } @@ -1075,27 +1075,27 @@ public void testOverrideEdgeWithRangeIndexAndTtl() { "date", "2019-12-23 16:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1123,29 +1123,29 @@ public void testOverrideEdgeWithRangeIndexAndTtl() { "date", "2019-12-23 16:01:00"); graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("date", "2019-12-23 14:01:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:01:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge8, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge10, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge6, edges.next()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1158,25 +1158,25 @@ public void testOverrideEdgeWithRangeIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("date", "2019-12-23 14:01:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:01:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:01:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:01:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:01:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:01:00")); Assert.assertFalse(edges.hasNext()); } @@ -1210,31 +1210,31 @@ public void testOverrideEdgeWithShardIndexAndTtl() { "date", "2019-12-23 14:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E().has("place", "home"); + Iterator edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge6, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge3, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge5, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge1, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1265,38 +1265,38 @@ public void testOverrideEdgeWithShardIndexAndTtl() { graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library"); + edges = graph().traversal().E().hasLabel("read").has("place", "library"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge12, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:01:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge9, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge11, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge7, edges.next()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:01:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1309,33 +1309,33 @@ public void testOverrideEdgeWithShardIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library"); + edges = graph().traversal().E().hasLabel("read").has("place", "library"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:01:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -1353,7 +1353,7 @@ public void testOverrideEdgeWithSearchIndexAndTtl() { "date", "2019-12-23 12:00:00"); graph().tx().commit(); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", Text.contains("school")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); @@ -1370,10 +1370,10 @@ public void testOverrideEdgeWithSearchIndexAndTtl() { graph().tx().commit(); // Due to overridden edges are expired, query will lead to async delete - edges = graph().traversal().E().has("place", Text.contains("school")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("school")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", Text.contains("city")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("city")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(edge, edges.next()); graph().tx().commit(); @@ -1385,7 +1385,7 @@ public void testOverrideEdgeWithSearchIndexAndTtl() { } // All edges are expired after 6s - edges = graph().traversal().E().has("place", Text.contains("library")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("library")); Assert.assertFalse(edges.hasNext()); } @@ -1445,7 +1445,7 @@ public void testQueryEdgeWithSecondaryIndexAndTtlInTx() { baby.addEdge("read", java2, "place", "library of school", "date", "2019-12-23 12:00:00"); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", "library of school"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1458,7 +1458,7 @@ public void testQueryEdgeWithSecondaryIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("place", "library of school"); + edges = graph().traversal().E().hasLabel("read").has("place", "library of school"); Assert.assertFalse(edges.hasNext()); } @@ -1503,27 +1503,27 @@ public void testQueryEdgeWithRangeIndexAndTtlInTx() { baby.addEdge("read", java10, "place", "library of school", "date", "2019-12-23 16:00:00"); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); @@ -1536,22 +1536,22 @@ public void testQueryEdgeWithRangeIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("date", "2019-12-23 14:00:00"); + edges = graph().traversal().E().hasLabel("read").has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E() + edges = graph().traversal().E().hasLabel("read") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -1603,31 +1603,31 @@ public void testQueryEdgeWithShardIndexAndTtlInTx() { baby.addEdge("read", java12, "place", "home", "date", "2019-12-23 14:00:00"); - Iterator edges = graph().traversal().E().has("place", "home"); + Iterator edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(4, IteratorUtils.count(edges)); @@ -1640,26 +1640,26 @@ public void testQueryEdgeWithShardIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("place", "home"); + edges = graph().traversal().E().hasLabel("read").has("place", "home"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", "2019-12-23 14:00:00"); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gt("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.gte("2019-12-23 15:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lt("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); - edges = graph().traversal().E().has("place", "library of school") + edges = graph().traversal().E().hasLabel("read").has("place", "library of school") .has("date", P.lte("2019-12-23 13:00:00")); Assert.assertFalse(edges.hasNext()); } @@ -1682,7 +1682,7 @@ public void testQueryEdgeWithSearchIndexAndTtlInTx() { baby.addEdge("read", java2, "place", "library of school", "date", "2019-12-23 12:00:00"); - Iterator edges = graph().traversal().E() + Iterator edges = graph().traversal().E().hasLabel("read") .has("place", Text.contains("library")); Assert.assertTrue(edges.hasNext()); Assert.assertEquals(2, IteratorUtils.count(edges)); @@ -1695,7 +1695,7 @@ public void testQueryEdgeWithSearchIndexAndTtlInTx() { } // All edges are expired after 3s - edges = graph().traversal().E().has("place", Text.contains("library")); + edges = graph().traversal().E().hasLabel("read").has("place", Text.contains("library")); Assert.assertFalse(edges.hasNext()); } @@ -4729,10 +4729,10 @@ public void testQueryByLongPropOfOverrideEdge() { louise.addEdge("strike", sean, "id", 1, "timestamp", current + 1, "place", "park", "tool", "shovel", "reason", "jeer", "arrested", false); - List edges = graph.traversal().E().has("timestamp", current) + List edges = graph.traversal().E().hasLabel("strike").has("timestamp", current) .toList(); Assert.assertEquals(0, edges.size()); - edges = graph.traversal().E().has("timestamp", current + 1).toList(); + edges = graph.traversal().E().hasLabel("strike").has("timestamp", current + 1).toList(); Assert.assertEquals(1, edges.size()); } @@ -4758,36 +4758,36 @@ public void testQueryByNegativeLongProperty() { graph.tx().commit(); - List edges = graph.traversal().E() + List edges = graph.traversal().E().hasLabel("transfer") .has("timestamp", -100L).toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(2), edges.get(0).value("id")); - edges = graph.traversal().E() + edges = graph.traversal().E().hasLabel("transfer") .has("timestamp", P.between(-101L, 0L)) .toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(2), edges.get(0).value("id")); - edges = graph.traversal().E().has("timestamp", P.gt(-101L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gt(-101L)).toList(); Assert.assertEquals(2, edges.size()); - edges = graph.traversal().E().has("timestamp", P.gte(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gte(-100L)).toList(); Assert.assertEquals(2, edges.size()); - edges = graph.traversal().E().has("timestamp", P.gt(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gt(-100L)).toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(1), edges.get(0).value("id")); - edges = graph.traversal().E().has("timestamp", P.gt(-99L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.gt(-99L)).toList(); Assert.assertEquals(1, edges.size()); Assert.assertEquals(IdGenerator.of(1), edges.get(0).value("id")); - edges = graph.traversal().E().has("timestamp", P.lt(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.lt(-100L)).toList(); Assert.assertEquals(0, edges.size()); - edges = graph.traversal().E().has("timestamp", P.lte(-100L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.lte(-100L)).toList(); Assert.assertEquals(1, edges.size()); - edges = graph.traversal().E().has("timestamp", P.lt(0L)).toList(); + edges = graph.traversal().E().hasLabel("transfer").has("timestamp", P.lt(0L)).toList(); Assert.assertEquals(1, edges.size()); } @@ -4807,17 +4807,17 @@ public void testQueryByRepeatedRangePredicates() { "timestamp", -4L, "message", "test"); graph.tx().commit(); - List edges = graph.traversal().E() + List edges = graph.traversal().E().hasLabel("transfer") .has("timestamp", -4L) .has("timestamp", P.lte(4L)).toList(); Assert.assertEquals(1, edges.size()); - long count = graph.traversal().E() + long count = graph.traversal().E().hasLabel("transfer") .has("timestamp", -4L) .has("timestamp", P.lte(4L)).count().next(); Assert.assertEquals(1L, count); - count = graph.traversal().E() + count = graph.traversal().E().hasLabel("transfer") .has("timestamp", P.lte(4L)) .has("timestamp", -4L).count().next(); Assert.assertEquals(1L, count); @@ -7081,10 +7081,10 @@ public void testQueryEdgeByStringPropOfOverrideEdge() { louise.addEdge("strike", sean, "id", 1, "timestamp", current, "place", "street", "tool", "shovel", "reason", "jeer", "arrested", false); - List edges = graph.traversal().E().has("place", "park") + List edges = graph.traversal().E().hasLabel("strike").has("place", "park") .toList(); Assert.assertEquals(0, edges.size()); - edges = graph.traversal().E().has("place", "street").toList(); + edges = graph.traversal().E().hasLabel("strike").has("place", "street").toList(); Assert.assertEquals(1, edges.size()); } @@ -7103,28 +7103,28 @@ public void testQueryEdgeBeforeAfterUpdateMultiPropertyWithIndex() { "tool", "shovel", "reason", "jeer", "arrested", false); - List vl = graph.traversal().E().has("tool", "shovel").toList(); + List vl = graph.traversal().E().hasLabel("strike").has("tool", "shovel").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); - vl = graph.traversal().E().has("timestamp", current).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", current).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); - vl = graph.traversal().E().has("tool", "knife").toList(); + vl = graph.traversal().E().hasLabel("strike").has("tool", "knife").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().E().has("timestamp", 666L).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(0, vl.size()); edge.property("tool", "knife"); edge.property("timestamp", 666L); - vl = graph.traversal().E().has("tool", "shovel").toList(); + vl = graph.traversal().E().hasLabel("strike").has("tool", "shovel").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().E().has("timestamp", current).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", current).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().E().has("tool", "knife").toList(); + vl = graph.traversal().E().hasLabel("strike").has("tool", "knife").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); - vl = graph.traversal().E().has("timestamp", 666L).toList(); + vl = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals(1, (int) vl.get(0).value("id")); } @@ -7175,18 +7175,18 @@ public void testQueryEdgeBeforeAfterUpdatePropertyWithRangeIndex() { "tool", "shovel", "reason", "jeer", "arrested", false); - List el = graph.traversal().E().has("timestamp", current) + List el = graph.traversal().E().hasLabel("strike").has("timestamp", current) .toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); - el = graph.traversal().E().has("timestamp", 666L).toList(); + el = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(0, el.size()); edge.property("timestamp", 666L); - el = graph.traversal().E().has("timestamp", current).toList(); + el = graph.traversal().E().hasLabel("strike").has("timestamp", current).toList(); Assert.assertEquals(0, el.size()); - el = graph.traversal().E().has("timestamp", 666L).toList(); + el = graph.traversal().E().hasLabel("strike").has("timestamp", 666L).toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); } @@ -7205,11 +7205,11 @@ public void testQueryEdgeWithNullablePropertyInCompositeIndex() { "timestamp", current, "place", "park", "tool", "shovel", "arrested", false); - List el = graph.traversal().E().has("place", "park") + List el = graph.traversal().E().hasLabel("strike").has("place", "park") .toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); - el = graph.traversal().E().has("place", "park") + el = graph.traversal().E().hasLabel("strike").has("place", "park") .has("tool", "shovel").toList(); Assert.assertEquals(1, el.size()); Assert.assertEquals(1, (int) el.get(0).value("id")); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java new file mode 100644 index 0000000000..534db06a0d --- /dev/null +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.core; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.hugegraph.backend.page.PageInfo; +import org.apache.hugegraph.backend.query.Condition; +import org.apache.hugegraph.backend.query.ConditionQuery; +import org.apache.hugegraph.exception.NoIndexException; +import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.traversal.optimize.Text; +import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.SchemaStatus; +import org.apache.tinkerpop.gremlin.process.traversal.P; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.structure.T; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.collect.ImmutableSet; + +public class SourceIndexCoverageTest extends BaseCoreTest { + + @Before + public void initSchema() { + SchemaManager schema = graph().schema(); + schema.propertyKey("coverageName").asText().create(); + schema.propertyKey("coverageScore").asInt().create(); + schema.propertyKey("coverageBody").asText().create(); + schema.vertexLabel("coverageA") + .properties("coverageName", "coverageScore", "coverageBody") + .useAutomaticId().create(); + schema.vertexLabel("coverageB") + .properties("coverageName", "coverageScore", "coverageBody") + .enableLabelIndex(false).useAutomaticId().create(); + schema.vertexLabel("unrelated").properties("coverageBody").useAutomaticId().create(); + } + + private Set addVertices() { + Vertex a = graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 10, "coverageBody", "gold silver"); + Vertex b = graph().addVertex(T.label, "coverageB", "coverageName", "same", + "coverageScore", 20, "coverageBody", "gold bronze"); + this.commitTx(); + return ImmutableSet.of(a, b); + } + + private void nameIndex(String label) { + graph().schema().indexLabel(label + "ByName").onV(label) + .by("coverageName").secondary().create(); + } + + @Test + public void testCompleteCandidatesOrExplicitFailure() { + this.nameIndex("coverageA"); + Set expected = this.addVertices(); + Set actual; + try { + actual = graph().traversal().V().has("coverageName", "same").toSet(); + } catch (NoIndexException e) { + return; + } + Assert.assertEquals(expected, actual); + } + + @Test + public void testPartialCoverageRejectsIncompleteSource() { + this.nameIndex("coverageA"); + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toSet(), error -> { + Assert.assertContains("Incomplete index coverage", error.getMessage()); + Assert.assertFalse(error.getMessage().contains("coverageB")); + }); + Assert.assertEquals(1, graph().traversal().V().hasLabel("coverageA") + .has("coverageName", "same").toList().size()); + this.nameIndex("coverageB"); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testEmptyCandidateLabelStillRequiresCoverage() { + this.nameIndex("coverageA"); + graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 10, "coverageBody", "gold"); + this.commitTx(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + } + + @Test + public void testNoCoverageAndExplicitIds() { + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + Object[] ids = expected.stream().map(Vertex::id).toArray(); + Assert.assertEquals(expected, + graph().traversal().V(ids).has("coverageName", "same").toSet()); + graph().schema().edgeLabel("coverageLink").link("coverageA", "coverageB").create(); + Vertex a = expected.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = expected.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + a.addEdge("coverageLink", b); + this.commitTx(); + Assert.assertEquals(ImmutableSet.of(b), graph().traversal().V(a.id()) + .out("coverageLink").has("coverageName", "same").toSet()); + } + + @Test + public void testDownstreamStepsCannotHidePartialCoverage() { + this.nameIndex("coverageA"); + this.addVertices(); + for (GraphTraversal query : new GraphTraversal[]{ + graph().traversal().V().has("coverageName", "same").limit(1), + graph().traversal().V().has("coverageName", "same").count(), + graph().traversal().V().has("coverageName", "same").out(), + graph().traversal().V().has("coverageName", "same").where(__.out()), + graph().traversal().V().has("coverageName", "same").barrier()}) { + Assert.assertThrows(NoIndexException.class, query::toList); + } + } + + @Test + public void testPreparedTraversalChecksCurrentSchema() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + this.addVertices(); + GraphTraversal query = graph().traversal().V().has("coverageName", "same"); + query.asAdmin().applyStrategies(); + graph().schema().vertexLabel("coverageLater").properties("coverageName") + .useAutomaticId().create(); + graph().addVertex(T.label, "coverageLater", "coverageName", "same"); + this.commitTx(); + Assert.assertThrows(NoIndexException.class, query::toList); + this.nameIndex("coverageLater"); + Assert.assertEquals(3, graph().traversal().V().has("coverageName", "same").toList().size()); + graph().schema().indexLabel("coverageBByName").remove(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + } + + @Test + public void testRangeRequiresCompatibleCoverage() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageAByScore").onV("coverageA").by("coverageScore").range().create(); + schema.indexLabel("coverageBByScore").onV("coverageB") + .by("coverageScore").secondary().create(); + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageScore", P.gte(10)).toList()); + // Equality is supported by both RANGE and SECONDARY indexes. + Assert.assertEquals(1, graph().traversal().V().has("coverageScore", 10).toList().size()); + schema.indexLabel("coverageBByScore").remove(); + schema.indexLabel("coverageBRange").onV("coverageB").by("coverageScore").range().create(); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageScore", P.gte(10)).toSet()); + } + + @Test + public void testSearchRequiresCompatibleCoverage() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageASearch").onV("coverageA").by("coverageBody").search().create(); + schema.indexLabel("coverageBSecondary").onV("coverageB") + .by("coverageBody").secondary().create(); + this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().V().has("coverageBody", Text.contains("(gold)")).toList()); + schema.indexLabel("coverageBSearch").onV("coverageB").by("coverageBody").search().create(); + schema.indexLabel("unrelatedSearch").onV("unrelated").by("coverageBody").search().create(); + Assert.assertEquals(2, graph().traversal().V() + .has("coverageBody", Text.contains("(gold)")).toList().size()); + } + + @Test + public void testCompositeCoverage() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageAComposite").onV("coverageA") + .by("coverageName", "coverageScore").secondary().create(); + this.nameIndex("coverageB"); + this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toList()); + schema.indexLabel("coverageBComposite").onV("coverageB") + .by("coverageName", "coverageScore").secondary().create(); + Assert.assertEquals(1, graph().traversal().V().has("coverageName", "same") + .has("coverageScore", 10).toList().size()); + } + + @Test + public void testJointIndexCoverage() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageARange").onV("coverageA") + .by("coverageScore").range().create(); + Set vertices = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").has("coverageScore", P.gte(15)).toList()); + schema.indexLabel("coverageBRange").onV("coverageB") + .by("coverageScore").range().create(); + Vertex expected = vertices.stream().filter(v -> v.label().equals("coverageB")) + .findFirst().get(); + Assert.assertEquals(ImmutableSet.of(expected), graph().traversal().V() + .has("coverageName", "same").has("coverageScore", P.gte(15)).toSet()); + } + + @Test + public void testIndexStatusAndRebuild() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + graph().schema().indexLabel("coverageBByName").rebuild(); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + IndexLabel index = graph().schema().getIndexLabel("coverageBByName"); + try { + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.CREATING, + SchemaStatus.REBUILDING, + SchemaStatus.DELETING}) { + this.params().schemaTransaction().updateSchemaStatus(index, status); + Assert.assertThrows(IllegalArgumentException.class, () -> + graph().traversal().V().has("coverageName", "same").toList()); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.CREATED); + } + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testGlobalEdgeCoverage() { + SchemaManager schema = graph().schema(); + schema.edgeLabel("edgeA").link("coverageA", "coverageB") + .properties("coverageName").create(); + schema.edgeLabel("edgeB").link("coverageA", "coverageB") + .properties("coverageName").enableLabelIndex(false).create(); + schema.indexLabel("edgeAByName").onE("edgeA").by("coverageName").secondary().create(); + this.addVertices(); + Iterator vertices = graph().traversal().V().toList().iterator(); + Vertex a = vertices.next(); + Vertex b = vertices.next(); + if (a.label().equals("coverageB")) { + Vertex swap = a; + a = b; + b = swap; + } + a.addEdge("edgeA", b, "coverageName", "same"); + a.addEdge("edgeB", b, "coverageName", "same"); + this.commitTx(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().E().has("coverageName", "same").toList()); + schema.indexLabel("edgeBByName").onE("edgeB").by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + } + + @Test + public void testParentEdgeIndexCoversSubLabels() { + Assume.assumeTrue(storeFeatures().supportsFatherAndSubEdgeLabel()); + SchemaManager schema = graph().schema(); + schema.edgeLabel("parentEdge").asBase().properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + for (String label : new String[]{"subA", "subB"}) { + schema.edgeLabel(label).withBase("parentEdge").link("coverageA", "coverageB") + .properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + } + schema.indexLabel("parentByName").onE("parentEdge") + .by("coverageName").secondary().create(); + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + a.addEdge("subA", b, "coverageName", "same", "coverageScore", 1); + a.addEdge("subB", b, "coverageName", "same", "coverageScore", 2); + this.commitTx(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + schema.indexLabel("parentByName").remove(); + schema.indexLabel("subAByName").onE("subA").by("coverageName").secondary().create(); + Assert.assertThrows(NoIndexException.class, () -> + graph().traversal().E().has("coverageName", "same").toList()); + schema.indexLabel("subBByName").onE("subB").by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + } + + @Test + public void testPagingRequiresCoverageOnEveryRequest() { + Assume.assumeTrue(storeFeatures().supportsQueryByPage()); + this.nameIndex("coverageA"); + Set expected = this.addVertices(); + Assert.assertThrows(NoIndexException.class, () -> this.page("")); + this.nameIndex("coverageB"); + Set actual = new HashSet<>(); + String page = ""; + String resume = null; + int pages = 0; + do { + ConditionQuery query = this.pageQuery(page); + Iterator iterator = graph().vertices(query); + try { + iterator.forEachRemaining(actual::add); + page = PageInfo.pageInfo(iterator); + if (resume == null) { + resume = page; + } + } finally { + CloseableIterator.closeIterator(iterator); + } + Assert.assertTrue(++pages < 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(expected, actual); + Assert.assertNotNull(resume); + String continuation = resume; + graph().schema().indexLabel("coverageBByName").remove(); + Assert.assertThrows(NoIndexException.class, () -> this.page(continuation)); + } + + private ConditionQuery pageQuery(String page) { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.eq(graph().propertyKey("coverageName").id(), "same")); + query.limit(1); + query.page(page); + return query; + } + + private void page(String page) { + Iterator iterator = graph().vertices(this.pageQuery(page)); + try { + while (iterator.hasNext()) { + iterator.next(); + } + } finally { + CloseableIterator.closeIterator(iterator); + } + } +} 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 335130ede5..3399394b78 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 @@ -1320,7 +1320,7 @@ public void testAddVertexWithSecondaryIndexAndTtl() { "age", 3, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Beijing"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1332,7 +1332,7 @@ public void testAddVertexWithSecondaryIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); } @@ -1355,23 +1355,23 @@ public void testAddVertexWithRangeIndexAndTtl() { "age", 7, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("age", 5); + Iterator vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); this.commitTx(); @@ -1382,19 +1382,19 @@ public void testAddVertexWithRangeIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("age", 5); + vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -1419,32 +1419,32 @@ public void testAddVertexWithShardIndexAndTtl() { "age", 5, "city", "Shanghai"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("city", + Iterator vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex6, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1456,26 +1456,26 @@ public void testAddVertexWithShardIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -1489,7 +1489,7 @@ public void testAddVertexWithSearchIndexAndTtl() { "age", 3, "city", "Beijing Haidian"); this.commitTx(); - Iterator vertices = graph().traversal().V().has( + Iterator vertices = graph().traversal().V().hasLabel("fan").has( "city", Text.contains("Haidian")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1501,7 +1501,7 @@ public void testAddVertexWithSearchIndexAndTtl() { // Ignore } - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Haidian")); Assert.assertFalse(vertices.hasNext()); } @@ -1541,7 +1541,7 @@ public void testOverrideVertexWithSecondaryIndexAndTtl() { "age", 3, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Beijing"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1560,10 +1560,10 @@ public void testOverrideVertexWithSecondaryIndexAndTtl() { // Due to overridden vertices are expired, // query will lead to async delete - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); this.commitTx(); @@ -1575,9 +1575,9 @@ public void testOverrideVertexWithSecondaryIndexAndTtl() { } // All edges are expired after 6s - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); } @@ -1600,23 +1600,23 @@ public void testOverrideVertexWithRangeIndexAndTtl() { "age", 11, "city", "Beijing"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("age", 7); + Iterator vertices = graph().traversal().V().hasLabel("fan").has("age", 7); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("age", P.gt(9)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(9)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("age", P.gte(9)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(9)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(5)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(5)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("age", P.lte(5)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(5)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); this.commitTx(); @@ -1645,26 +1645,26 @@ public void testOverrideVertexWithRangeIndexAndTtl() { * Due to overridden vertices are expired, * query will lead to async delete */ - vertices = graph().traversal().V().has("age", 7); + vertices = graph().traversal().V().hasLabel("fan").has("age", 7); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", 8); + vertices = graph().traversal().V().hasLabel("fan").has("age", 8); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex8, vertices.next()); - vertices = graph().traversal().V().has("age", P.gt(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(10)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex10, vertices.next()); - vertices = graph().traversal().V().has("age", P.gte(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(10)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex6, vertices.next()); - vertices = graph().traversal().V().has("age", P.lte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); this.commitTx(); @@ -1676,22 +1676,22 @@ public void testOverrideVertexWithRangeIndexAndTtl() { } // All edges are expired after 6s - vertices = graph().traversal().V().has("age", 7); + vertices = graph().traversal().V().hasLabel("fan").has("age", 7); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", 8); + vertices = graph().traversal().V().hasLabel("fan").has("age", 8); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gt(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(10)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gte(10)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(10)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(6)); Assert.assertFalse(vertices.hasNext()); } @@ -1716,32 +1716,32 @@ public void testOverrideVertexWithShardIndexAndTtl() { "age", 5, "city", "Shanghai"); this.commitTx(); - Iterator vertices = graph().traversal().V().has("city", + Iterator vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex6, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex3, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(7)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex5, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(7)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(3)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex1, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(3)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1773,38 +1773,38 @@ public void testOverrideVertexWithShardIndexAndTtl() { this.commitTx(); // Due to overridden vertices are expired, query will lead to async delete - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shenzhen"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shenzhen"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex12, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 6); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex9, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(8)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex11, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(8)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex7, vertices.next()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1817,33 +1817,33 @@ public void testOverrideVertexWithShardIndexAndTtl() { } // All vertices are expired after 6s - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Shenzhen"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shenzhen"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 6); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(8)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(8)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -1857,7 +1857,7 @@ public void testOverrideVertexWithSearchIndexAndTtl() { "age", 3, "city", "Beijing Haidian"); this.commitTx(); - Iterator vertices = graph().traversal().V().has( + Iterator vertices = graph().traversal().V().hasLabel("fan").has( "city", Text.contains("Haidian")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1874,11 +1874,11 @@ public void testOverrideVertexWithSearchIndexAndTtl() { this.commitTx(); // Due to overridden vertices are expired, query will lead to async delete - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Haidian")); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Pudong")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(vertex, vertices.next()); @@ -1891,7 +1891,7 @@ public void testOverrideVertexWithSearchIndexAndTtl() { } // All vertices are expired after 6s - vertices = graph().traversal().V().has("city", + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Pudong")); Assert.assertFalse(vertices.hasNext()); } @@ -1940,7 +1940,7 @@ public void testQueryVertexWithSecondaryIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby2", "age", 3, "city", "Beijing"); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Beijing"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -1953,7 +1953,7 @@ public void testQueryVertexWithSecondaryIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("city", "Beijing"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing"); Assert.assertFalse(vertices.hasNext()); } @@ -1986,23 +1986,23 @@ public void testQueryVertexWithRangeIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby10", "age", 7, "city", "Beijing"); - Iterator vertices = graph().traversal().V().has("age", 5); + Iterator vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); this.commitTx(); @@ -2014,19 +2014,19 @@ public void testQueryVertexWithRangeIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("age", 5); + vertices = graph().traversal().V().hasLabel("fan").has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gt(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.gte(6)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lt(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("age", P.lte(4)); + vertices = graph().traversal().V().hasLabel("fan").has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -2063,32 +2063,32 @@ public void testQueryVertexWithShardIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby12", "age", 5, "city", "Shanghai"); - Iterator vertices = graph().traversal().V() + Iterator vertices = graph().traversal().V().hasLabel("fan") .has("city", "Shanghai"); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(4, IteratorUtils.count(vertices)); @@ -2101,26 +2101,26 @@ public void testQueryVertexWithShardIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("city", "Shanghai"); + vertices = graph().traversal().V().hasLabel("fan").has("city", "Shanghai"); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", 5); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gt(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.gte(6)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lt(4)); Assert.assertFalse(vertices.hasNext()); - vertices = graph().traversal().V().has("city", "Beijing") + vertices = graph().traversal().V().hasLabel("fan").has("city", "Beijing") .has("age", P.lte(4)); Assert.assertFalse(vertices.hasNext()); } @@ -2137,7 +2137,7 @@ public void testQueryVertexWithSearchIndexAndTtlInTx() { graph().addVertex(T.label, "fan", "name", "Baby2", "age", 3, "city", "Beijing Haidian"); - Iterator vertices = graph().traversal().V().has( + Iterator vertices = graph().traversal().V().hasLabel("fan").has( "city", Text.contains("Haidian")); Assert.assertTrue(vertices.hasNext()); Assert.assertEquals(2, IteratorUtils.count(vertices)); @@ -2150,7 +2150,7 @@ public void testQueryVertexWithSearchIndexAndTtlInTx() { } // All vertices are expired after 3s - vertices = graph().traversal().V().has("city", Text.contains("Haidian")); + vertices = graph().traversal().V().hasLabel("fan").has("city", Text.contains("Haidian")); Assert.assertFalse(vertices.hasNext()); } @@ -3621,15 +3621,15 @@ public void testQueryByIntPropWithDifferentDataType() { initPersonIndex(false); init5Persons(); - List vertices = graph.traversal().V().has("age", 21).toList(); + List vertices = graph.traversal().V().hasLabel("person").has("age", 21).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertThrows(IllegalArgumentException.class, () -> { - graph.traversal().V().has("age", 21.0).toList(); + graph.traversal().V().hasLabel("person").has("age", 21.0).toList(); }); Assert.assertThrows(IllegalArgumentException.class, () -> { - graph.traversal().V().has("age", "21").toList(); + graph.traversal().V().hasLabel("person").has("age", "21").toList(); }); } @@ -3645,41 +3645,41 @@ public void testQueryByIntPropWithNegativeNumber() { "city", "Beijing", "age", -10, "birth", Utils.date("2029-01-01")); - List vertices = graph.traversal().V().has("age", -10).toList(); + List vertices = graph.traversal().V().hasLabel("person").has("age", -10).toList(); Assert.assertEquals(1, vertices.size()); assertContains(vertices, T.label, "person", "name", "Sean", "city", "Beijing", "age", -10, "birth", Utils.date("2029-01-01")); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("age", P.between(-11, 0)) .toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Sean", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.gt(-11)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gt(-11)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V().has("age", P.gte(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gte(-10)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V().has("age", P.gt(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gt(-10)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Louise", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.gt(-9)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.gt(-9)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Louise", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.lt(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.lt(-10)).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V().has("age", P.lte(-10)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.lte(-10)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Sean", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("age", P.lt(0)).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", P.lt(0)).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("Sean", vertices.get(0).value("name")); } @@ -4935,9 +4935,9 @@ public void testQueryByIntPropOfOverrideVertex() { "city", "Hongkong", "age", 29); this.mayCommitTx(); - List vertices = graph.traversal().V().has("age", 28).toList(); + List vertices = graph.traversal().V().hasLabel("person").has("age", 28).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V().has("age", 29).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 29).toList(); Assert.assertEquals(1, vertices.size()); } @@ -4952,10 +4952,10 @@ public void testQueryByStringPropOfOverrideVertex() { "city", "Hongkong", "age", 29); this.mayCommitTx(); - List vertices = graph.traversal().V().has("city", "Beijing") + List vertices = graph.traversal().V().hasLabel("person").has("city", "Beijing") .toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V().has("city", "Hongkong").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); } @@ -5005,7 +5005,7 @@ public void testQueryWithTxNotCommittedByIntProp() { "age", 18, "city", "Beijing"); List vertices; - vertices = graph.traversal().V().has("age", 18).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 18).toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals(18, (int) vertices.get(0).value("age")); @@ -5070,41 +5070,41 @@ public void testQueryByJointIndexes() { List vertices; // results count < 2 - vertices = graph.traversal().V().has("age", 3).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V().has("city", "Hongkong").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 2).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Taipei").has("age", 3).toList(); Assert.assertEquals(0, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Hongkong").has("age", 3) .skip(1).toList(); Assert.assertEquals(0, vertices.size()); // results count >= 2 - vertices = graph.traversal().V().has("age", 20).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 20).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V().has("age", 19).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 19).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph.traversal().V().has("city", "Beijing").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Beijing").toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Beijing").has("age", P.lt(21)).toList(); Assert.assertEquals(3, vertices.size()); @@ -5117,11 +5117,11 @@ public void testQueryByJointIndexes() { Whitebox.setInternalState(tx, "indexTx.indexIntersectThresh", i); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Beijing").has("age", 20).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph.traversal().V() + vertices = graph.traversal().V().hasLabel("person") .has("city", "Beijing").has("age", 20) .skip(1).toList(); Assert.assertEquals(1, vertices.size()); @@ -5140,11 +5140,11 @@ public void testQueryByJointIndexesAndCompositeIndexForOneLabel() { this.commitTx(); List vertices; - vertices = graph().traversal().V().has("age", 3).toList(); + vertices = graph().traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong").toList(); + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong") + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong") .has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); @@ -5155,11 +5155,11 @@ public void testQueryByJointIndexesAndCompositeIndexForOneLabel() { graph().schema().indexLabel("personByCityAndAge").onV("person") .by("city", "age").ifNotExist().create(); - vertices = graph().traversal().V().has("age", 3).toList(); + vertices = graph().traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong").toList(); + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V().has("city", "Hongkong") + vertices = graph().traversal().V().hasLabel("person").has("city", "Hongkong") .has("age", 3).toList(); Assert.assertEquals(1, vertices.size()); } @@ -5187,7 +5187,7 @@ public void testQueryByJointIndexesAndCompositeIndexForMultiLabel() { this.mayCommitTx(); List vertices; - vertices = graph().traversal().V().has("age", 3) + vertices = graph().traversal().V().hasLabel("dog", "cat").has("age", 3) .has("city", "Hongkong").toList(); Assert.assertEquals(2, vertices.size()); Set labels = new HashSet<>(); @@ -5211,7 +5211,7 @@ public void testQueryByJointIndexesOnlyWithCompositeIndex() { "city", "Hongkong", "age", 3); this.mayCommitTx(); - List vertices = graph().traversal().V().has("age", 3) + List vertices = graph().traversal().V().hasLabel("dog").has("age", 3) .has("city", "Hongkong") .has("name", "Tom").toList(); Assert.assertEquals(1, vertices.size()); @@ -5225,7 +5225,7 @@ public void testQueryByJointIndexesWithRangeIndex() { "city", "Hongkong", "age", 3); List vertices; - vertices = graph().traversal().V().has("age", P.gt(2)) + vertices = graph().traversal().V().hasLabel("person").has("age", P.gt(2)) .has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); } @@ -5243,7 +5243,7 @@ public void testQueryByJointIndexesWithCompositeIndexIncludeOtherField() { "city", "Hongkong", "age", 3); this.mayCommitTx(); - List vertices = graph().traversal().V().has("age", P.gt(2)) + List vertices = graph().traversal().V().hasLabel("dog").has("age", P.gt(2)) .has("city", "Hongkong").toList(); Assert.assertEquals(1, vertices.size()); } @@ -5537,47 +5537,47 @@ public void testQueryByShardIndex() { "city", "Beijing", "age", 29); this.mayCommitTx(); - List vertices = graph().traversal().V() + List vertices = graph().traversal().V().hasLabel("person") .has("city", "Hongkong") .toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Hongkong") .has("age", 15).toList(); Assert.assertEquals(1, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Hongkong") .has("age", P.between(10, 20)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.between(20, 30)).toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.lt(29)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.lte(29)).toList(); Assert.assertEquals(3, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.gt(21)).toList(); Assert.assertEquals(2, vertices.size()); - vertices = graph().traversal().V() + vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .has("age", P.gte(21)).toList(); Assert.assertEquals(3, vertices.size()); @@ -6392,15 +6392,15 @@ public void testAddVertexPropertyWithSpecialValueForSecondaryIndex() { List vertices; - vertices = graph.traversal().V().has("city", "b\u0001").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "b\u0001").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("1", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("city", "c\u0002").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "c\u0002").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("2", vertices.get(0).value("name")); - vertices = graph.traversal().V().has("city", "d\u0003").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "d\u0003").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("3", vertices.get(0).value("name")); @@ -6423,7 +6423,7 @@ public void testAddVertexPropertyWithSpecialValueForSecondaryIndex() { "city", "a\u0000", "age", 0); this.commitTx(); - vertices = graph.traversal().V().has("city", "a\u0000").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "a\u0000").toList(); Assert.assertEquals(1, vertices.size()); Assert.assertEquals("0", vertices.get(0).value("name")); } @@ -6594,7 +6594,7 @@ public void testQueryVertexByPropertyWithEmptyString() { graph.addVertex(T.label, "person", "name", "Baby", "city", ""); this.commitTx(); - Vertex vertex = graph.traversal().V().has("city", "").next(); + Vertex vertex = graph.traversal().V().hasLabel("person").has("city", "").next(); Assert.assertEquals("Baby", vertex.value("name")); Assert.assertEquals("", vertex.value("city")); } @@ -6689,29 +6689,29 @@ public void testQueryVertexBeforeAfterUpdateMultiPropertyWithIndex() { "city", "Hongkong", "age", 3); this.commitTx(); - List vl = graph.traversal().V().has("age", 3).toList(); + List vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("city", "Hongkong").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(0, vl.size()); vertex.property("age", 5); vertex.property("city", "Shanghai"); this.commitTx(); - vl = graph.traversal().V().has("age", 3).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("city", "Hongkong").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); } @@ -6725,18 +6725,18 @@ public void testQueryVertexBeforeAfterUpdatePropertyWithSecondaryIndex() { "city", "Hongkong", "age", 3); this.commitTx(); - List vl = graph.traversal().V().has("city", "Hongkong").toList(); + List vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(0, vl.size()); vertex.property("city", "Shanghai"); this.commitTx(); - vl = graph.traversal().V().has("city", "Hongkong").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Hongkong").toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("city", "Shanghai").toList(); + vl = graph.traversal().V().hasLabel("person").has("city", "Shanghai").toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); } @@ -6750,18 +6750,18 @@ public void testQueryVertexBeforeAfterUpdatePropertyWithRangeIndex() { "city", "Hongkong", "age", 3); this.commitTx(); - List vl = graph.traversal().V().has("age", 3).toList(); + List vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(0, vl.size()); vertex.property("age", 5); this.commitTx(); - vl = graph.traversal().V().has("age", 3).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 3).toList(); Assert.assertEquals(0, vl.size()); - vl = graph.traversal().V().has("age", 5).toList(); + vl = graph.traversal().V().hasLabel("person").has("age", 5).toList(); Assert.assertEquals(1, vl.size()); Assert.assertEquals("Baby", vl.get(0).value("name")); } @@ -8891,17 +8891,8 @@ public void testQueryByUnionIndexInPageWithSomeIndexNoData() { storeFeatures().supportsQueryByPage()); HugeGraph graph = graph(); - SchemaManager schema = graph.schema(); GraphTraversalSource g = graph.traversal(); - initPageTestData(); - - // Author has index by name but no data - schema.indexLabel("authorByName") - .onV("author") - .by("name") - .secondary() - .ifNotExist() - .create(); + initPageTestData(true); GraphTraversal iter = g.V().has("name", "marko") .has("~page", "").limit(1); @@ -8936,21 +8927,21 @@ public void testQueryBySecondaryIndexWithLimitAndOffset() { initPersonIndex(true); init5Persons(); - List vertices = graph().traversal().V() + List vertices = graph().traversal().V().hasLabel("person") .has("city", "Beijing").toList(); Assert.assertEquals(3, vertices.size()); - List verticesSkip = graph().traversal().V() + List verticesSkip = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .skip(1).toList(); Assert.assertEquals(2, verticesSkip.size()); - Set vertices1 = graph().traversal().V() + Set vertices1 = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .range(0, 2).toSet(); Assert.assertEquals(2, vertices1.size()); - Set vertices2 = graph().traversal().V() + Set vertices2 = graph().traversal().V().hasLabel("person") .has("city", "Beijing") .range(2, 3).toSet(); Assert.assertEquals(1, vertices2.size()); @@ -8965,20 +8956,20 @@ public void testQueryByRangeIndexWithLimitAndOffset() { initPersonIndex(false); init5Persons(); - List vertices = graph().traversal().V() + List vertices = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)).toList(); Assert.assertEquals(4, vertices.size()); - List verticesSkip = graph().traversal().V() + List verticesSkip = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)) .skip(1).toList(); Assert.assertEquals(3, verticesSkip.size()); - Set vertices1 = graph().traversal().V() + Set vertices1 = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)) .range(0, 3).toSet(); Assert.assertEquals(3, vertices1.size()); - Set vertices2 = graph().traversal().V() + Set vertices2 = graph().traversal().V().hasLabel("person") .has("age", P.between(5, 22)) .range(3, 4).toSet(); Assert.assertEquals(1, vertices2.size()); @@ -9348,19 +9339,19 @@ public void testQueryBySearchIndexWithSpecialSymbol() { Assert.assertEquals(vertex6.value("city"), city); List vertices; - vertices = g.V().has("city", Text.contains("abc")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("abc")).toList(); Assert.assertEquals(3, vertices.size()); Assert.assertTrue(vertices.contains(vertex1)); Assert.assertTrue(vertices.contains(vertex3)); Assert.assertTrue(vertices.contains(vertex5)); - vertices = g.V().has("city", Text.contains("\u0002")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("\u0002")).toList(); Assert.assertEquals(0, vertices.size()); - vertices = g.V().has("city", Text.contains("\u0003")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("\u0003")).toList(); Assert.assertEquals(0, vertices.size()); - vertices = g.V().has("city", Text.contains("\u0001")).toList(); + vertices = g.V().hasLabel("person").has("city", Text.contains("\u0001")).toList(); Assert.assertEquals(0, vertices.size()); String backend = graph.backend(); @@ -9397,6 +9388,15 @@ public void testEnhanceTextMatch() { graph.schema().indexLabel("personByName").onV("person") .by("name").search().ifNotExist().create(); + // Keep the global SEARCH path: qualifying person would select its + // primary-key path instead. All candidate labels need SEARCH coverage. + for (VertexLabel label : graph.schema().getVertexLabels()) { + if (!label.name().equals("person") && + label.properties().contains(graph.propertyKey("name").id())) { + graph.schema().indexLabel(label.name() + "ByNameSearchCoverage") + .onV(label.name()).by("name").search().create(); + } + } Vertex vertex1 = graph.addVertex(T.label, "person", "name", "秦始皇", "city", "Hongkong", "age", 15); @@ -9534,8 +9534,25 @@ private void init5Computers() { } private void initPageTestData() { + this.initPageTestData(false); + } + + private void initPageTestData(boolean emptyIndexFirst) { SchemaManager schema = graph().schema(); + // These global paging tests require complete source-index coverage. + // Keep their two-label fixture separate from the unrelated labels + // created by initSchema(); partial coverage has dedicated regressions. + for (VertexLabel label : schema.getVertexLabels()) { + schema.vertexLabel(label.name()).remove(); + } schema.propertyKey("lang").asText().ifNotExist().create(); + if (emptyIndexFirst) { + // Keep the empty index ahead of the populated labels so the union + // test still exercises continuation past an empty first source. + schema.vertexLabel("author").properties("name").useAutomaticId().create(); + schema.indexLabel("authorByName").onV("author") + .by("name").secondary().create(); + } schema.vertexLabel("programmer") .properties("name", "age", "city") From f435a1f5891afc04549bc5e2723e7cde8472134b Mon Sep 17 00:00:00 2001 From: contrueCT Date: Mon, 28 Sep 2026 19:10:44 +0800 Subject: [PATCH 2/8] fix(core): preserve OLAP and invalid-value query behavior --- .../backend/tx/GraphIndexTransaction.java | 10 ++++- .../core/SourceIndexCoverageTest.java | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) 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 d84917470a..718917f39c 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 @@ -814,7 +814,10 @@ private Set collectMatchedIndexes(ConditionQuery query, matchedIndexes.add(index); } } - if (requireCompleteCoverage && !matchedIndexes.isEmpty()) { + // An invalid value returns no results after an index is found. Keep + // that result before deciding whether index coverage is complete. + if (requireCompleteCoverage && !matchedIndexes.isEmpty() && + validQueryConditionValues(this.graph(), query)) { this.checkIndexCoverage(query, schemaLabels, matchedIndexes); } return matchedIndexes; @@ -830,6 +833,11 @@ private void checkIndexCoverage(ConditionQuery query, // OLAP properties are shared rather than declared on every label. requiredProperties.removeIf(id -> this.graph().propertyKey(id).olap()); } + if (requiredProperties.isEmpty()) { + // A shared OLAP index can match several labels, while MatchedIndex + // deduplicates those matches by index labels alone. + return; + } Set covered = new HashSet<>(); for (MatchedIndex index : indexes) { covered.add(index.schemaLabel().id()); diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index 534db06a0d..c82d4d5711 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -30,7 +30,9 @@ import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.type.HugeType; +import org.apache.hugegraph.type.define.GraphReadMode; import org.apache.hugegraph.type.define.SchemaStatus; +import org.apache.hugegraph.type.define.WriteType; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; @@ -41,6 +43,7 @@ import org.junit.Before; import org.junit.Test; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; public class SourceIndexCoverageTest extends BaseCoreTest { @@ -103,6 +106,48 @@ public void testPartialCoverageRejectsIncompleteSource() { graph().traversal().V().has("coverageName", "same").toSet()); } + @Test + public void testInvalidValuePrecedesPartialCoverageError() { + this.addVertices(); + ConditionQuery invalid = new ConditionQuery(HugeType.VERTEX); + invalid.query(Condition.eq(graph().propertyKey("coverageScore").id(), "not-an-int")); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(invalid).hasNext()); + + graph().schema().indexLabel("coverageAByScore").onV("coverageA") + .by("coverageScore").secondary().create(); + Assert.assertEquals(ImmutableList.of(), ImmutableList.copyOf(graph().vertices(invalid))); + + ConditionQuery valid = new ConditionQuery(HugeType.VERTEX); + valid.query(Condition.eq(graph().propertyKey("coverageScore").id(), 10)); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(valid).hasNext()); + } + + @Test + public void testOlapOnlyQueryAcrossLabels() { + // The existing OLAP secondary-property query also returns no results + // on HStore master; its backend coverage is tracked in #3090. + Assume.assumeFalse("HStore OLAP secondary query is tracked in #3090", + "hstore".equals(graph().backend())); + Assume.assumeTrue("Not support olap properties", + storeFeatures().supportsOlapProperties()); + graph().schema().propertyKey("coverageOlap").asText() + .writeType(WriteType.OLAP_SECONDARY).create(); + Set expected = this.addVertices(); + for (Vertex vertex : expected) { + graph().addVertex(T.id, vertex.id(), "coverageOlap", "same"); + } + this.commitTx(); + + GraphReadMode previous = graph().readMode(); + graph().readMode(GraphReadMode.ALL); + try { + Assert.assertEquals(expected, + graph().traversal().V().has("coverageOlap", "same").toSet()); + } finally { + graph().readMode(previous); + } + } + @Test public void testEmptyCandidateLabelStillRequiresCoverage() { this.nameIndex("coverageA"); From 0de33eda10ccc07d87a0ed0b9980996e8c31aa89 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Tue, 29 Sep 2026 14:02:41 +0800 Subject: [PATCH 3/8] fix(test): complete TinkerPop fixture index coverage --- .../core/SourceIndexCoverageTest.java | 62 ++++++++++++++++--- .../apache/hugegraph/tinkerpop/TestGraph.java | 28 +++++++++ 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index c82d4d5711..98c9098ab7 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -28,14 +28,17 @@ import org.apache.hugegraph.schema.IndexLabel; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.tinkerpop.TestGraph; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphReadMode; +import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.type.define.WriteType; import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +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; @@ -78,16 +81,59 @@ private void nameIndex(String label) { } @Test - public void testCompleteCandidatesOrExplicitFailure() { + public void testCompleteCandidatesReturnAllResults() { this.nameIndex("coverageA"); + this.nameIndex("coverageB"); Set expected = this.addVertices(); - Set actual; - try { - actual = graph().traversal().V().has("coverageName", "same").toSet(); - } catch (NoIndexException e) { - return; - } - Assert.assertEquals(expected, actual); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testTinkerPopModernSchemaCoversGlobalPropertyQueries() { + new TestGraph(graph()).initModernSchema(IdStrategy.AUTOMATIC); + Vertex person = graph().addVertex(T.label, "person", "name", "marko", "age", 29); + Vertex software = graph().addVertex(T.label, "software", "name", "lop"); + Edge created = person.addEdge("created", software, "weight", 0.4d); + this.commitTx(); + + Assert.assertEquals(ImmutableSet.of(person), + graph().traversal().V().has("name", "marko").toSet()); + Assert.assertEquals(ImmutableSet.of(person), + graph().traversal().V().has("age", 29).toSet()); + Assert.assertEquals(ImmutableSet.of(created), + graph().traversal().E().has("weight", 0.4d).toSet()); + } + + @Test + public void testTinkerPopBasicSchemaCoversSharedProperties() { + new TestGraph(graph()).initBasicSchema(IdStrategy.AUTOMATIC, TestGraph.DEFAULT_VL); + Vertex vertex = graph().addVertex(T.label, TestGraph.DEFAULT_VL, "name", "shared"); + Vertex person = graph().addVertex(T.label, "person", "name", "shared"); + Vertex software = graph().addVertex(T.label, "software", "name", "shared"); + Edge self = vertex.addEdge("self", vertex, "name", "shared"); + Edge friend = vertex.addEdge("friend", vertex, "name", "shared"); + Edge link = vertex.addEdge("l", vertex, "name", "shared"); + Edge partition = vertex.addEdge("aTOa", vertex, + "gremlin.partitionGraphStrategy.partition", "shared"); + this.commitTx(); + + Assert.assertEquals(ImmutableSet.of(vertex, person, software), + graph().traversal().V().has("name", "shared").toSet()); + Assert.assertEquals(ImmutableSet.of(self, friend, link), + graph().traversal().E().has("name", "shared").toSet()); + Assert.assertEquals(ImmutableSet.of(partition), graph().traversal().E() + .has("gremlin.partitionGraphStrategy.partition", "shared").toSet()); + } + + @Test + public void testTinkerPopSinkSchemaCoversName() { + new TestGraph(graph()).initSinkSchema(); + Vertex message = graph().addVertex(T.label, "message", "name", "ping"); + this.commitTx(); + + Assert.assertEquals(ImmutableSet.of(message), + graph().traversal().V().has("name", "ping").toSet()); } @Test diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java index 53dc1fe9ac..31a035a6eb 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/tinkerpop/TestGraph.java @@ -533,6 +533,10 @@ public void initModernSchema(IdStrategy idStrategy) { .ifNotExist().create(); schema.indexLabel("dogByName").onV("dog").by("name") .ifNotExist().create(); + schema.indexLabel("animalByName").onV("animal").by("name") + .ifNotExist().create(); + schema.indexLabel("animalByAge").onV("animal").by("age").range() + .ifNotExist().create(); schema.indexLabel("vertexByName").onV("vertex").by("name") .ifNotExist().create(); schema.indexLabel("vertexByAge").onV("vertex").by("age").range() @@ -541,6 +545,8 @@ public void initModernSchema(IdStrategy idStrategy) { .ifNotExist().create(); schema.indexLabel("createdByWeight").onE("created").by("weight") .range().ifNotExist().create(); + schema.indexLabel("createdByEdgeWeight").onE("createdBy").by("weight") + .range().ifNotExist().create(); schema.indexLabel("personByNameAge").onV("person").by("name", "age") .ifNotExist().create(); schema.indexLabel("vertexByP").onV("vertex").by("p") @@ -855,6 +861,16 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { schema.indexLabel("selfByName").onE("self").by("name") .ifNotExist().create(); + if (!"person".equals(defaultVL)) { + schema.indexLabel("personByName").onV("person").by("name") + .ifNotExist().create(); + } + schema.indexLabel("softwareByName").onV("software").by("name") + .ifNotExist().create(); + schema.indexLabel("friendByName").onE("friend").by("name") + .ifNotExist().create(); + schema.indexLabel("lByName").onE("l").by("name") + .ifNotExist().create(); schema.indexLabel("selfBy__id").onE("self").by("__id") .ifNotExist().create(); schema.indexLabel("selfBySome").onE("self").by("some") @@ -869,12 +885,21 @@ private void initBasicVertexLabelAndEdgeLabelExceptV(String defaultVL) { schema.indexLabel("aTOaByGremlinPartition").onE("aTOa") .by("gremlin.partitionGraphStrategy.partition") .ifNotExist().create(); + schema.indexLabel("aTObByGremlinPartition").onE("aTOb") + .by("gremlin.partitionGraphStrategy.partition") + .ifNotExist().create(); schema.indexLabel("aTOcByGremlinPartition").onE("aTOc") .by("gremlin.partitionGraphStrategy.partition") .ifNotExist().create(); schema.indexLabel("bTOcByGremlinPartition").onE("bTOc") .by("gremlin.partitionGraphStrategy.partition") .ifNotExist().create(); + schema.indexLabel("connectsToByGremlinPartition").onE("connectsTo") + .by("gremlin.partitionGraphStrategy.partition") + .ifNotExist().create(); + schema.indexLabel("relatesToByGremlinPartition").onE("relatesTo") + .by("gremlin.partitionGraphStrategy.partition") + .ifNotExist().create(); schema.edgeLabel("blah1").link(defaultVL, defaultVL) .ifNotExist().create(); schema.edgeLabel("blah2").link(defaultVL, defaultVL) @@ -942,5 +967,8 @@ public void initSinkSchema() { schema.indexLabel("loopsByName").onV("loops") .secondary().by("name") .ifNotExist().create(); + schema.indexLabel("messageByName").onV("message") + .secondary().by("name") + .ifNotExist().create(); } } From 7f5a91738d910137fd2c91806073c00043c4483e Mon Sep 17 00:00:00 2001 From: contrueCT Date: Fri, 2 Oct 2026 13:08:37 +0800 Subject: [PATCH 4/8] fix(server): align index coverage with query visibility --- .../backend/tx/GraphIndexTransaction.java | 12 ++ .../apache/hugegraph/example/Example1.java | 4 +- .../core/SourceIndexCoverageTest.java | 152 ++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphIndexTransaction.java index 718917f39c..51e8ccebfb 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 @@ -806,6 +806,18 @@ private Set collectMatchedIndexes(ConditionQuery query, } } + if (requireCompleteCoverage) { + // Match the public result visibility, including flattened queries. + // Removal jobs drop indexes before data/schema; those labels must + // neither require coverage nor contribute unavailable index plans. + // Cleanup callers still need to inspect invisible labels' indexes. + Query visibility = query.rootOriginQuery(); + schemaLabels = schemaLabels.stream().filter(schemaLabel -> + (visibility.showHidden() || !schemaLabel.hidden()) && + (visibility.showDeleting() || !schemaLabel.status().deleting())) + .collect(Collectors.toList()); + } + // Collect MatchedIndex for each SchemaLabel Set matchedIndexes = InsertionOrderUtil.newSet(); for (SchemaLabel schemaLabel : schemaLabels) { diff --git a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java index a6cda3932f..1d800ce796 100644 --- a/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java +++ b/hugegraph-server/hugegraph-example/src/main/java/org/apache/hugegraph/example/Example1.java @@ -479,10 +479,10 @@ public static void testLeftIndexProcess(final HugeGraph graph) { .has("city", "Hangzhou").toList(); assert vertices.isEmpty(); // set breakpoint here to see secondary_indexes and range_indexes table - vertices = graph.traversal().V().has("age", 28).toList(); + vertices = graph.traversal().V().hasLabel("person").has("age", 28).toList(); assert vertices.isEmpty(); // set breakpoint here to see secondary_indexes and range_indexes table - vertices = graph.traversal().V().has("city", "Hangzhou").toList(); + vertices = graph.traversal().V().hasLabel("person").has("city", "Hangzhou").toList(); assert vertices.isEmpty(); } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index 98c9098ab7..06506d5b47 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -26,6 +26,7 @@ import org.apache.hugegraph.backend.query.ConditionQuery; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.schema.IndexLabel; +import org.apache.hugegraph.schema.SchemaLabel; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.tinkerpop.TestGraph; @@ -343,6 +344,139 @@ public void testIndexStatusAndRebuild() { graph().traversal().V().has("coverageName", "same").toSet()); } + @Test + public void testDeletingVertexLabelUsesQueryVisibility() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + Vertex visible = expected.stream().filter(v -> v.label().equals("coverageA")) + .findFirst().get(); + SchemaLabel deleting = graph().schema().getVertexLabel("coverageB"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + IndexLabel index = graph().schema().getIndexLabel("coverageBByName"); + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.DELETING); + // A hidden label's unavailable index must not be validated either. + Assert.assertEquals(ImmutableSet.of(visible), graph().traversal().V() + .has("coverageName", "same").toSet()); + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.CREATED); + ConditionQuery includeDeleting = this.nameQuery(HugeType.VERTEX); + includeDeleting.showDeleting(true); + Assert.assertEquals(expected, ImmutableSet.copyOf(graph().vertices(includeDeleting))); + graph().schema().indexLabel("coverageBByName").remove(); + + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.DELETING, + SchemaStatus.UNDELETED}) { + this.params().schemaTransaction().updateSchemaStatus(deleting, status); + Assert.assertEquals(ImmutableSet.of(visible), graph().traversal().V() + .has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(query).hasNext()); + // Flattened queries inherit visibility from the outer request. + ConditionQuery child = query.copy(); + child.showDeleting(false); + child.setOriginQuery(query); + Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(child).hasNext()); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").toSet()); + this.nameIndex("coverageB"); + Assert.assertEquals(expected, + graph().traversal().V().has("coverageName", "same").toSet()); + } + + @Test + public void testDeletingEdgeLabelUsesQueryVisibility() { + SchemaManager schema = graph().schema(); + for (String label : new String[]{"visibleEdge", "deletingEdge"}) { + schema.edgeLabel(label).link("coverageA", "coverageB") + .properties("coverageName").create(); + schema.indexLabel(label + "ByName").onE(label) + .by("coverageName").secondary().create(); + } + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + Edge visible = a.addEdge("visibleEdge", b, "coverageName", "same"); + Edge removed = a.addEdge("deletingEdge", b, "coverageName", "same"); + this.commitTx(); + SchemaLabel deleting = schema.getEdgeLabel("deletingEdge"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + ConditionQuery includeDeleting = this.nameQuery(HugeType.EDGE); + includeDeleting.showDeleting(true); + Assert.assertEquals(ImmutableSet.of(visible, removed), + ImmutableSet.copyOf(graph().edges(includeDeleting))); + schema.indexLabel("deletingEdgeByName").remove(); + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.DELETING, + SchemaStatus.UNDELETED}) { + this.params().schemaTransaction().updateSchemaStatus(deleting, status); + Assert.assertEquals(ImmutableSet.of(visible), graph().traversal().E() + .has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, () -> graph().edges(query).hasNext()); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .has("coverageName", "same").toSet()); + schema.indexLabel("deletingEdgeByName").onE("deletingEdge") + .by("coverageName").secondary().create(); + Assert.assertEquals(ImmutableSet.of(visible, removed), graph().traversal().E() + .has("coverageName", "same").toSet()); + } + + @Test + public void testPagingWhileVertexLabelIsDeleting() { + Assume.assumeTrue(storeFeatures().supportsQueryByPage()); + this.nameIndex("coverageA"); + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex another = graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 30, "coverageBody", "gold"); + this.commitTx(); + SchemaLabel deleting = graph().schema().getVertexLabel("coverageB"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + Set actual = new HashSet<>(); + String page = ""; + int requests = 0; + do { + Iterator iterator = graph().vertices(this.pageQuery(page)); + try { + while (iterator.hasNext()) { + Assert.assertTrue(actual.add(iterator.next())); + } + page = PageInfo.pageInfo(iterator); + } finally { + CloseableIterator.closeIterator(iterator); + } + this.params().schemaTransaction().updateSchemaStatus(deleting, + SchemaStatus.UNDELETED); + Assert.assertTrue(++requests < 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(ImmutableSet.of(a, another), actual); + Assert.assertTrue(requests > 1); + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> this.page("")); + } + + private ConditionQuery nameQuery(HugeType type) { + ConditionQuery query = new ConditionQuery(type); + query.query(Condition.eq(graph().propertyKey("coverageName").id(), "same")); + return query; + } + @Test public void testGlobalEdgeCoverage() { SchemaManager schema = graph().schema(); @@ -398,6 +532,24 @@ public void testParentEdgeIndexCoversSubLabels() { schema.indexLabel("subBByName").onE("subB").by("coverageName").secondary().create(); Assert.assertEquals(2, graph().traversal().E().has("coverageName", "same").toList().size()); + + SchemaLabel deleting = schema.getEdgeLabel("subB"); + try { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + schema.indexLabel("subBByName").remove(); + Assert.assertEquals(ImmutableList.of("subA"), graph().traversal().E() + .has("coverageName", "same").label().toList()); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, () -> graph().edges(query).hasNext()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .has("coverageName", "same").toList()); + schema.indexLabel("subBByName").onE("subB").by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); } @Test From 15d87e19a2a0634e17fec602ea4f4f17fa401f82 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Thu, 15 Oct 2026 19:36:50 +0800 Subject: [PATCH 5/8] fix(server): handle invisible query candidates - Return empty results and pages for visibility-excluded candidates. - Cover unlabeled composite and joint plans in one query. - Adapt merged traversal fixtures and shared schema imports. - Document property-index coverage and visibility rules. --- docs/server-runtime.md | 26 +++ .../backend/tx/GraphIndexTransaction.java | 86 ++++++--- .../core/SourceIndexCoverageTest.java | 178 +++++++++++++++++- .../apache/hugegraph/core/VertexCoreTest.java | 49 +++-- 4 files changed, 288 insertions(+), 51 deletions(-) diff --git a/docs/server-runtime.md b/docs/server-runtime.md index 5a27fb6b0c..89fe9b5a06 100644 --- a/docs/server-runtime.md +++ b/docs/server-runtime.md @@ -251,6 +251,32 @@ without a preceding REST graph query, and subsequent REST queries must remain us Gremlin scripts still cannot read restricted system properties such as `java.version` or disable the security manager. +### Property-query index coverage + +A global property query such as `g.V().has("name", "alice")` or +`g.E().has("weight", 0.5)` requires compatible indexes on every visible candidate +label. A candidate declares all queried non-OLAP properties; even a label with no +records requires coverage. Base edge labels have no direct records; their indexes +can cover sub-labels. Shared OLAP indexes follow the graph read mode. + +| Candidate state | Query behavior | +| --- | --- | +| Every visible candidate has compatible indexes | The query combines the indexed results. Composite and joint index plans may serve different labels in the same query. | +| A visible candidate lacks a compatible index | The query fails with `NoIndexException`, including when other candidates are hidden or deleting. | +| Every candidate is excluded by visibility | The query returns an empty result, including after those labels' indexes have been removed. | + +Normal queries exclude hidden labels and labels in `DELETING` or `UNDELETED` +status. Internal requests that enable `showHidden` or `showDeleting` include the +corresponding labels and still require their indexes. Visibility follows the outer +request when a query is split into subqueries. Index state and complete coverage +are checked before backend iterators are opened; this does not provide a snapshot +against concurrent schema changes. + +To fix incomplete coverage, specify an indexed label, for example +`g.V().hasLabel("person").has("name", "alice")`, or add indexes compatible with the +query predicates to every candidate label. Explicit-ID lookups and adjacency +filtering do not introduce a global property-index coverage requirement. + ### Predicate and value compatibility Version-specific handling retains negated predicates and filtering barriers where 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 e0c816131d..a9eaf5b685 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 @@ -366,7 +366,31 @@ private IdHolderList queryByUserprop(ConditionQuery query) { query.conditionValues(HugeKeys.LABEL).isEmpty()) { return IdHolderList.empty(paging); } - Set indexes = this.collectMatchedIndexes(query, true); + List schemaLabels = this.querySchemaLabels(query); + // Use the outer request's visibility, including flattened queries. + Query visibility = query.rootOriginQuery(); + List visibleLabels = schemaLabels.stream().filter(label -> + (visibility.showHidden() || !label.hidden()) && + (visibility.showDeleting() || !label.status().deleting())) + .collect(Collectors.toList()); + if (visibleLabels.size() != schemaLabels.size()) { + Set properties = this.requiredIndexProperties(query); + boolean hadCandidates = schemaLabels.stream().anyMatch(label -> + isIndexQueryCandidate(label, properties)); + boolean hasVisibleCandidates = visibleLabels.stream().anyMatch(label -> + isIndexQueryCandidate(label, properties)); + // Only visibility exclusions justify an empty result here. Visible + // candidates without usable indexes must still fail below. + if (hadCandidates && !hasVisibleCandidates) { + IdHolderList empty = new IdHolderList(paging); + if (paging) { + // Keep a page slot so an empty first page is valid. + empty.add(new PagingIdHolder(query, q -> PageIds.EMPTY)); + } + return empty; + } + } + Set indexes = this.collectMatchedIndexes(query, visibleLabels); if (indexes.isEmpty()) { Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); throw noIndexException(this.graph(), query, label); @@ -377,6 +401,8 @@ private IdHolderList queryByUserprop(ConditionQuery query) { return IdHolderList.empty(paging); } + this.checkIndexCoverage(query, visibleLabels, indexes); + // Validate the whole plan before opening any backend iterators. A later // label's unavailable index must not leave earlier iterators unowned. for (MatchedIndex index : indexes) { @@ -654,20 +680,14 @@ private PageIds doIndexQueryOnce(IndexLabel indexLabel, } } - private Set collectMatchedIndexes(ConditionQuery query) { - return this.collectMatchedIndexes(query, false); - } - - @Watched(prefix = "index") - private Set collectMatchedIndexes(ConditionQuery query, - boolean requireCompleteCoverage) { + private List querySchemaLabels(ConditionQuery query) { ISchemaTransaction schema = this.params().schemaTransaction(); Set labels = query.conditionValues(HugeKeys.LABEL); List schemaLabels; if (query.containsConditionValues(HugeKeys.LABEL) && labels.isEmpty()) { // An empty intersection is not an unrestricted label query. - return Collections.emptySet(); + return Collections.emptyList(); } if (labels.size() == 1) { Id label = (Id) labels.iterator().next(); @@ -697,18 +717,17 @@ private Set collectMatchedIndexes(ConditionQuery query, } } - if (requireCompleteCoverage) { - // Match the public result visibility, including flattened queries. - // Removal jobs drop indexes before data/schema; those labels must - // neither require coverage nor contribute unavailable index plans. - // Cleanup callers still need to inspect invisible labels' indexes. - Query visibility = query.rootOriginQuery(); - schemaLabels = schemaLabels.stream().filter(schemaLabel -> - (visibility.showHidden() || !schemaLabel.hidden()) && - (visibility.showDeleting() || !schemaLabel.status().deleting())) - .collect(Collectors.toList()); - } + return schemaLabels; + } + + private Set collectMatchedIndexes(ConditionQuery query) { + // Cleanup callers must still inspect invisible labels' indexes. + return this.collectMatchedIndexes(query, this.querySchemaLabels(query)); + } + @Watched(prefix = "index") + private Set collectMatchedIndexes(ConditionQuery query, + List schemaLabels) { // Collect MatchedIndex for each SchemaLabel Set matchedIndexes = InsertionOrderUtil.newSet(); for (SchemaLabel schemaLabel : schemaLabels) { @@ -717,25 +736,30 @@ private Set collectMatchedIndexes(ConditionQuery query, matchedIndexes.add(index); } } - // An invalid value returns no results after an index is found. Keep - // that result before deciding whether index coverage is complete. - if (requireCompleteCoverage && !matchedIndexes.isEmpty() && - validQueryConditionValues(this.graph(), query)) { - this.checkIndexCoverage(query, schemaLabels, matchedIndexes); - } return matchedIndexes; } - private void checkIndexCoverage(ConditionQuery query, - List labels, - Set indexes) { - // Check at execution, before creating any lazy index iterator. Cleanup - // callers only need matching indexes and must not require full coverage. + private static boolean isIndexQueryCandidate(SchemaLabel label, Set properties) { + // Base edge labels describe sub-labels and have no direct records. + return label.properties().containsAll(properties) && + !(label instanceof EdgeLabel && ((EdgeLabel) label).isFather()); + } + + private Set requiredIndexProperties(ConditionQuery query) { Set requiredProperties = query.userpropKeys(); if (this.graph().readMode().showOlap()) { // OLAP properties are shared rather than declared on every label. requiredProperties.removeIf(id -> this.graph().propertyKey(id).olap()); } + return requiredProperties; + } + + private void checkIndexCoverage(ConditionQuery query, + List labels, + Set indexes) { + // Check at execution, before creating any lazy index iterator. Cleanup + // callers only need matching indexes and must not require full coverage. + Set requiredProperties = this.requiredIndexProperties(query); if (requiredProperties.isEmpty()) { // A shared OLAP index can match several labels, while MatchedIndex // deduplicates those matches by index labels alone. diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index 06506d5b47..478bff6a68 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -22,12 +22,13 @@ import java.util.Set; import org.apache.hugegraph.backend.page.PageInfo; -import org.apache.hugegraph.backend.query.Condition; -import org.apache.hugegraph.backend.query.ConditionQuery; import org.apache.hugegraph.exception.NoIndexException; -import org.apache.hugegraph.schema.IndexLabel; -import org.apache.hugegraph.schema.SchemaLabel; +import org.apache.hugegraph.query.Condition; +import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.schema.SchemaManager; +import org.apache.hugegraph.struct.schema.IndexLabel; +import org.apache.hugegraph.struct.schema.SchemaLabel; +import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.tinkerpop.TestGraph; import org.apache.hugegraph.traversal.optimize.Text; @@ -320,6 +321,145 @@ public void testJointIndexCoverage() { .has("coverageName", "same").has("coverageScore", P.gte(15)).toSet()); } + @Test + public void testUnlabeledMixedCompositeAndJointIndexes() { + SchemaManager schema = graph().schema(); + schema.indexLabel("coverageAComposite").onV("coverageA") + .by("coverageName", "coverageScore").secondary().create(); + this.nameIndex("coverageB"); + schema.indexLabel("coverageBRange").onV("coverageB") + .by("coverageScore").range().create(); + Set expected = this.addVertices(); + for (Vertex vertex : expected) { + vertex.property("coverageScore", 10); + } + // Each individual index also matches a vertex outside the intersection. + graph().addVertex(T.label, "coverageB", "coverageName", "other", + "coverageScore", 10, "coverageBody", "gold"); + graph().addVertex(T.label, "coverageB", "coverageName", "same", + "coverageScore", 30, "coverageBody", "gold"); + this.commitTx(); + Assert.assertEquals(expected, graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toSet()); + + IndexLabel index = schema.getIndexLabel("coverageBRange"); + try { + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.REBUILDING); + Assert.assertThrows(IllegalArgumentException.class, () -> graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toSet()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(index, SchemaStatus.CREATED); + } + Assert.assertEquals(expected, graph().traversal().V() + .has("coverageName", "same").has("coverageScore", 10).toSet()); + } + + @Test + public void testAllVertexCandidatesDeletingReturnEmpty() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + Set expected = this.addVertices(); + SchemaLabel a = graph().schema().getVertexLabel("coverageA"); + SchemaLabel b = graph().schema().getVertexLabel("coverageB"); + try { + for (SchemaStatus status : new SchemaStatus[]{SchemaStatus.DELETING, + SchemaStatus.UNDELETED}) { + this.params().schemaTransaction().updateSchemaStatus(a, status); + this.params().schemaTransaction().updateSchemaStatus(b, status); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("coverageName", "same").toSet()); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .hasLabel("coverageA").has("coverageName", "same").toSet()); + this.assertEmptyPage(this.nameQuery(HugeType.VERTEX)); + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.showDeleting(true); + Assert.assertEquals(expected, ImmutableSet.copyOf(graph().vertices(query))); + ConditionQuery child = query.copy(); + child.showDeleting(false); + child.setOriginQuery(query); + Assert.assertEquals(expected, ImmutableSet.copyOf(graph().vertices(child))); + } + graph().schema().indexLabel("coverageAByName").remove(); + graph().schema().indexLabel("coverageBByName").remove(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.showDeleting(true); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(query).hasNext()); + // A visible, unindexed candidate must still fail next to an invisible one. + this.params().schemaTransaction().updateSchemaStatus(a, SchemaStatus.CREATED); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("coverageName", "same").toSet()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(a, SchemaStatus.CREATED); + this.params().schemaTransaction().updateSchemaStatus(b, SchemaStatus.CREATED); + } + } + + @Test + public void testOnlyEdgeCandidateDeletingReturnsEmpty() { + SchemaManager schema = graph().schema(); + schema.edgeLabel("deletingEdge").link("coverageA", "coverageB") + .properties("coverageName").create(); + schema.indexLabel("deletingEdgeByName").onE("deletingEdge") + .by("coverageName").secondary().create(); + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + Edge edge = a.addEdge("deletingEdge", b, "coverageName", "same"); + this.commitTx(); + SchemaLabel label = schema.getEdgeLabel("deletingEdge"); + try { + this.params().schemaTransaction().updateSchemaStatus(label, SchemaStatus.DELETING); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().E() + .has("coverageName", "same").toSet()); + this.assertEmptyPage(this.nameQuery(HugeType.EDGE)); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.showDeleting(true); + Assert.assertEquals(ImmutableSet.of(edge), ImmutableSet.copyOf(graph().edges(query))); + schema.indexLabel("deletingEdgeByName").remove(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().E() + .has("coverageName", "same").toSet()); + Assert.assertThrows(NoIndexException.class, () -> graph().edges(query).hasNext()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(label, SchemaStatus.CREATED); + } + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .has("coverageName", "same").toSet()); + } + + @Test + public void testOnlyHiddenCandidateReturnsEmpty() { + SchemaManager schema = graph().schema(); + schema.propertyKey("hiddenName").asText().create(); + VertexLabel hidden = schema.vertexLabel("~coverageHidden") + .properties("hiddenName").useAutomaticId().build(); + this.params().schemaTransaction().addVertexLabel(hidden); + try { + schema.indexLabel("hiddenByName").onV("~coverageHidden") + .by("hiddenName").secondary().create(); + Vertex vertex = this.params().graphTransaction().addVertex( + this.params().graphTransaction().constructVertex( + false, T.label, "~coverageHidden", "hiddenName", "same")); + this.commitTx(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("hiddenName", "same").toSet()); + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.eq(graph().propertyKey("hiddenName").id(), "same")); + this.assertEmptyPage(query.copy()); + query.showHidden(true); + Assert.assertEquals(ImmutableSet.of(vertex), ImmutableSet.copyOf(graph().vertices(query))); + schema.indexLabel("hiddenByName").remove(); + Assert.assertEquals(ImmutableSet.of(), graph().traversal().V() + .has("hiddenName", "same").toSet()); + Assert.assertThrows(NoIndexException.class, () -> graph().vertices(query).hasNext()); + } finally { + // System schema cannot be deleted individually. Reset the test + // graph so the hidden label cannot retain this test's property key. + CoreTestSuite.clear(); + } + } + @Test public void testIndexStatusAndRebuild() { this.nameIndex("coverageA"); @@ -550,6 +690,20 @@ public void testParentEdgeIndexCoversSubLabels() { schema.indexLabel("subBByName").onE("subB").by("coverageName").secondary().create(); Assert.assertEquals(2, graph().traversal().E().has("coverageName", "same").toList().size()); + + SchemaLabel subA = schema.getEdgeLabel("subA"); + try { + this.params().schemaTransaction().updateSchemaStatus(subA, SchemaStatus.DELETING); + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.DELETING); + schema.indexLabel("subAByName").remove(); + schema.indexLabel("subBByName").remove(); + // The visible base label has no direct records of its own. + Assert.assertEquals(ImmutableSet.of(), graph().traversal().E() + .has("coverageName", "same").toSet()); + } finally { + this.params().schemaTransaction().updateSchemaStatus(subA, SchemaStatus.CREATED); + this.params().schemaTransaction().updateSchemaStatus(deleting, SchemaStatus.CREATED); + } } @Test @@ -584,6 +738,22 @@ public void testPagingRequiresCoverageOnEveryRequest() { Assert.assertThrows(NoIndexException.class, () -> this.page(continuation)); } + private void assertEmptyPage(ConditionQuery query) { + if (!storeFeatures().supportsQueryByPage()) { + return; + } + query.limit(1); + query.page(""); + Iterator iterator = query.resultType().isVertex() ? graph().vertices(query) : + graph().edges(query); + try { + Assert.assertFalse(iterator.hasNext()); + Assert.assertNull(PageInfo.pageInfo(iterator)); + } finally { + CloseableIterator.closeIterator(iterator); + } + } + private ConditionQuery pageQuery(String page) { ConditionQuery query = new ConditionQuery(HugeType.VERTEX); query.query(Condition.eq(graph().propertyKey("coverageName").id(), "same")); 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 2cfaa355d8..63cbc5c844 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 @@ -9344,36 +9344,41 @@ public void testNegativeLabelAfterAdjacencyPreservesSourceCandidates() { "age", 20, "city", "Beijing"); Vertex person = graph.addVertex(T.label, "person", "name", "loop-person", "age", 20, "city", "Beijing"); - fan.addEdge("fanLoop", fan); + Edge fanEdge = 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. + // A downstream label filter cannot repair incomplete source coverage. + Assert.assertThrows(NoIndexException.class, () -> + g.V().has("city", "Beijing").out().hasLabel(P.neq("author")).toSet()); + for (String label : new String[]{"fan", "follower", "returnFan"}) { + graph.schema().indexLabel(label + "ByCity").onV(label) + .by("city").secondary().create(); + } Set source = g.V().has("city", "Beijing").toSet(); - Assert.assertEquals(ImmutableSet.of(person), source); + Assert.assertEquals(ImmutableSet.of(person, fan), source); Assert.assertEquals(source, g.V().has("city", "Beijing").out().toSet()); - Assert.assertEquals(source, g.V().has("city", "Beijing") + Assert.assertEquals(ImmutableSet.of(person), 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") + Assert.assertEquals(ImmutableList.of(fan, 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(ImmutableSet.of(person), g.V().has("city", "Beijing") + .out().hasLabel(P.neq("author")).order().by("name").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") + Assert.assertEquals(ImmutableList.of(20, 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") + Assert.assertEquals(ImmutableList.of(1, 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() + Assert.assertEquals(ImmutableMap.of("person", ImmutableList.of(person), + "returnFan", ImmutableList.of(fan)), 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()); @@ -9387,13 +9392,13 @@ public void testNegativeLabelAfterAdjacencyPreservesSourceCandidates() { .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") + Assert.assertEquals(ImmutableSet.of(personEdge, fanEdge), g.V().has("city", "Beijing") .outE().hasLabel(P.neq("knows")).toSet()); - Assert.assertEquals(ImmutableSet.of(personEdge), g.V().has("city", "Beijing") + Assert.assertEquals(ImmutableSet.of(personEdge, fanEdge), 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")).order().by(T.label).tail(1).toSet()); + Assert.assertEquals(ImmutableSet.of(personEdge, fanEdge), 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()); @@ -9544,6 +9549,12 @@ public void testNegativeLabelAfterElementChange() { source.addEdge("link", target); this.commitTx(); GraphTraversalSource g = graph.traversal(); + Assert.assertThrows(NoIndexException.class, () -> + g.V().has("city", "Beijing").out().hasLabel(P.neq("indexed")).toSet()); + for (String label : new String[]{"person", "fan", "follower"}) { + graph.schema().indexLabel(label + "ByCity").onV(label) + .by("city").secondary().create(); + } 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"))))) { @@ -9670,6 +9681,12 @@ public void testRangeIndexAfterElementChangeWithTerminalSteps() { source.addEdge("link", target); this.commitTx(); GraphTraversalSource g = graph.traversal(); + Assert.assertThrows(NoIndexException.class, () -> g.V().has("age", P.gte(60)) + .out().hasLabel(P.neq("indexed")).count().next()); + for (String label : new String[]{"person", "author", "fan", "follower", "unindexed"}) { + graph.schema().indexLabel(label + "ByAge").onV(label) + .by("age").range().create(); + } 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(), From 4a9427c30668eca628c746d5a6e6d0db3c5ed7d7 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Thu, 15 Oct 2026 20:25:43 +0800 Subject: [PATCH 6/8] fix(server): close cursors on planning failure - Close cursors accumulated from earlier OR branches on failure. - Preserve the planning error and suppress cleanup failures. - Verify real-backend open/close counts and successful ownership. - Clarify per-branch coverage and cursor cleanup documentation. --- docs/server-runtime.md | 8 +- .../backend/tx/GraphTransaction.java | 42 +++-- .../core/SourceIndexCoverageTest.java | 151 ++++++++++++++++++ 3 files changed, 183 insertions(+), 18 deletions(-) diff --git a/docs/server-runtime.md b/docs/server-runtime.md index 89fe9b5a06..b3c4ed8dfd 100644 --- a/docs/server-runtime.md +++ b/docs/server-runtime.md @@ -268,9 +268,11 @@ can cover sub-labels. Shared OLAP indexes follow the graph read mode. Normal queries exclude hidden labels and labels in `DELETING` or `UNDELETED` status. Internal requests that enable `showHidden` or `showDeleting` include the corresponding labels and still require their indexes. Visibility follows the outer -request when a query is split into subqueries. Index state and complete coverage -are checked before backend iterators are opened; this does not provide a snapshot -against concurrent schema changes. +request when a query is split into subqueries. Each branch checks index state and +complete coverage before opening its backend iterators. If a later branch fails +planning, the query closes cursors accumulated from earlier branches and preserves +the original error. This does not provide a snapshot against concurrent schema +changes. To fix incomplete coverage, specify an indexed label, for example `g.V().hasLabel("person").has("name", "alice")`, or add indexes compatible with the 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 b0093b924c..3b55a141bb 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 @@ -1523,23 +1523,35 @@ private QueryList optimizeQueries(Query query, return queries; } - boolean supportIn = this.storeFeatures().supportsQueryWithInCondition(); - for (ConditionQuery cq : ConditionQueryFlatten.flatten( - (ConditionQuery) query, supportIn)) { - // Optimize by sysprop - Query q = this.optimizeQuery(cq); - /* - * NOTE: There are two possibilities for the returned q: - * 1.sysprop-query, which would not be empty. - * 2.index-query result(ids after optimization), which may be empty. - */ - if (q == null) { - queries.add(this.indexQuery(cq), this.batchSize); - } else if (!q.empty()) { - queries.add(q); + try { + boolean supportIn = this.storeFeatures().supportsQueryWithInCondition(); + for (ConditionQuery cq : ConditionQueryFlatten.flatten( + (ConditionQuery) query, supportIn)) { + // Optimize by sysprop + Query q = this.optimizeQuery(cq); + /* + * NOTE: There are two possibilities for the returned q: + * 1.sysprop-query, which would not be empty. + * 2.index-query result(ids after optimization), which may be empty. + */ + if (q == null) { + queries.add(this.indexQuery(cq), this.batchSize); + } else if (!q.empty()) { + queries.add(q); + } + } + return queries; + } catch (Throwable failure) { + // Earlier flattened branches may already own backend cursors. + try { + queries.close(); + } catch (Throwable closing) { + if (closing != failure) { + failure.addSuppressed(closing); + } } + throw QueryBatch.propagate(failure); } - return queries; } private Query optimizeQuery(ConditionQuery query) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index 478bff6a68..6efd68c6f0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -17,19 +17,26 @@ package org.apache.hugegraph.core; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Set; import org.apache.hugegraph.backend.page.PageInfo; +import org.apache.hugegraph.backend.store.BackendEntry; +import org.apache.hugegraph.backend.store.BackendStore; +import org.apache.hugegraph.backend.tx.GraphIndexTransaction; import org.apache.hugegraph.exception.NoIndexException; 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.struct.schema.IndexLabel; import org.apache.hugegraph.struct.schema.SchemaLabel; import org.apache.hugegraph.struct.schema.VertexLabel; import org.apache.hugegraph.testutil.Assert; +import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.tinkerpop.TestGraph; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.type.HugeType; @@ -47,7 +54,10 @@ import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mockito; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -154,6 +164,147 @@ public void testPartialCoverageRejectsIncompleteSource() { graph().traversal().V().has("coverageName", "same").toSet()); } + @Test + public void testOrCoverageFailureClosesEarlierBranchCursors() { + this.prepareOrIndexes(false); + this.addVertices(); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + NoIndexException failure = Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(this.orQuery())); + Assert.assertContains("Incomplete index coverage", failure.getMessage()); + tracker.assertCounts(2, 1); + Assert.assertEquals(0, failure.getSuppressed().length); + // Closing must happen before rollback or test-fixture cleanup. + graph().tx().rollback(); + tracker.assertCounts(2, 1); + } + } + + @Test + public void testOrCoverageFailurePreservesOriginalErrorWhenCloseFails() { + this.prepareOrIndexes(false); + this.addVertices(); + RuntimeException closeFailure = new IllegalStateException("index cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closeFailure)) { + NoIndexException failure = Assert.assertThrows(NoIndexException.class, + () -> graph().vertices(this.orQuery())); + Assert.assertContains("Incomplete index coverage", failure.getMessage()); + // A failed close on the first cursor must not skip the second one. + tracker.assertCounts(2, 1); + Assert.assertEquals(1, failure.getSuppressed().length); + Assert.assertSame(closeFailure, Throwables.getRootCause(failure.getSuppressed()[0])); + } + } + + @Test + public void testCompleteOrPlanKeepsCursorsUntilResultClose() { + this.prepareOrIndexes(true); + Set expected = this.addVertices(); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Iterator results = graph().vertices(this.orQuery()); + try { + tracker.assertCounts(4, 0); + Assert.assertTrue(results.hasNext()); + Assert.assertTrue(expected.contains(results.next())); + } finally { + CloseableIterator.closeIterator(results); + } + // Includes the later OR branch whose results were never requested. + tracker.assertCounts(4, 1); + } + } + + private void prepareOrIndexes(boolean complete) { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + graph().schema().indexLabel("coverageAByScore").onV("coverageA") + .by("coverageScore").secondary().create(); + if (complete) { + graph().schema().indexLabel("coverageBByScore").onV("coverageB") + .by("coverageScore").secondary().create(); + } + } + + private ConditionQuery orQuery() { + ConditionQuery query = new ConditionQuery(HugeType.VERTEX); + query.query(Condition.or( + Condition.eq(graph().propertyKey("coverageName").id(), "same"), + Condition.eq(graph().propertyKey("coverageScore").id(), 10))); + return query; + } + + private final class IndexCursorTracker implements AutoCloseable { + + private final GraphIndexTransaction indexTx; + private final BackendStore original; + private final List cursors = new ArrayList<>(); + + private IndexCursorTracker(RuntimeException closeFailure) { + this.indexTx = Whitebox.getInternalState(params().graphTransaction(), "indexTx"); + this.original = Whitebox.getInternalState(this.indexTx, "store"); + BackendStore observed = Mockito.mock(BackendStore.class, + AdditionalAnswers.delegatesTo(this.original)); + Mockito.doAnswer(invocation -> { + // Keep real index matching, validation, serialization and backend queries. + Iterator entries = this.original.query((Query) invocation.getArgument(0)); + CountingIndexCursor cursor = new CountingIndexCursor(entries, + this.cursors.isEmpty() ? closeFailure : null); + this.cursors.add(cursor); + return cursor; + }).when(observed).query(Mockito.any(Query.class)); + Whitebox.setInternalState(this.indexTx, "store", observed); + } + + private void assertCounts(int opened, int closesPerCursor) { + Assert.assertEquals(opened, this.cursors.size()); + for (CountingIndexCursor cursor : this.cursors) { + Assert.assertEquals(closesPerCursor, cursor.closes); + } + } + + @Override + public void close() { + Whitebox.setInternalState(this.indexTx, "store", this.original); + // Release leaks even when the regression assertion fails on old code. + for (CountingIndexCursor cursor : this.cursors) { + if (cursor.closes == 0) { + CloseableIterator.closeIterator(cursor.entries); + } + } + } + } + + private static final class CountingIndexCursor implements CloseableIterator { + + private final Iterator entries; + private final RuntimeException closeFailure; + private int closes; + + private CountingIndexCursor(Iterator entries, RuntimeException closeFailure) { + this.entries = entries; + this.closeFailure = closeFailure; + } + + @Override + public boolean hasNext() { + return this.entries.hasNext(); + } + + @Override + public BackendEntry next() { + return this.entries.next(); + } + + @Override + public void close() { + this.closes++; + CloseableIterator.closeIterator(this.entries); + if (this.closeFailure != null) { + throw this.closeFailure; + } + } + } + @Test public void testInvalidValuePrecedesPartialCoverageError() { this.addVertices(); From 83ac535454310e53b4357be40b68b036a67c4284 Mon Sep 17 00:00:00 2001 From: contrueCT Date: Fri, 16 Oct 2026 11:28:20 +0800 Subject: [PATCH 7/8] test(server): verify negative label query path - Reproduce the root negative-label query with an unindexed excluded label - Assert local filtering, exact results and zero index cursor opens - Clarify property-type short circuit in the query coverage documentation --- docs/server-runtime.md | 18 +++++++---- .../core/SourceIndexCoverageTest.java | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/server-runtime.md b/docs/server-runtime.md index b3c4ed8dfd..28f6ff114d 100644 --- a/docs/server-runtime.md +++ b/docs/server-runtime.md @@ -253,18 +253,24 @@ or disable the security manager. ### Property-query index coverage -A global property query such as `g.V().has("name", "alice")` or -`g.E().has("weight", 0.5)` requires compatible indexes on every visible candidate -label. A candidate declares all queried non-OLAP properties; even a label with no -records requires coverage. Base edge labels have no direct records; their indexes -can cover sub-labels. Shared OLAP indexes follow the graph read mode. +For type-compatible values, a global property-index query such as +`g.V().has("name", "alice")` or `g.E().has("weight", 0.5)` requires compatible +indexes on every visible candidate label. A candidate declares all queried non-OLAP +properties; even a label with no records requires coverage. Base edge labels have no +direct records; their indexes can cover sub-labels. Shared OLAP indexes follow the +graph read mode. | Candidate state | Query behavior | | --- | --- | | Every visible candidate has compatible indexes | The query combines the indexed results. Composite and joint index plans may serve different labels in the same query. | -| A visible candidate lacks a compatible index | The query fails with `NoIndexException`, including when other candidates are hidden or deleting. | +| A visible candidate lacks a compatible index and the values pass the property-type check | The query fails with `NoIndexException`, including when other candidates are hidden or deleting. | | Every candidate is excluded by visibility | The query returns an empty result, including after those labels' indexes have been removed. | +When query-visible candidates remain, index matching precedes the property-type +check. If at least one index matches but a value fails that check, the query returns +an empty result before complete coverage is checked. If no index matches, it still +raises `NoIndexException`, even for a type-mismatched value. + Normal queries exclude hidden labels and labels in `DELETING` or `UNDELETED` status. Internal requests that enable `showHidden` or `showDeleting` include the corresponding labels and still require their indexes. Visibility follows the outer diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index 6efd68c6f0..11cc517ba0 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -38,6 +38,7 @@ import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.Whitebox; import org.apache.hugegraph.tinkerpop.TestGraph; +import org.apache.hugegraph.traversal.optimize.HugeGraphStep; import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphReadMode; @@ -305,6 +306,37 @@ public void close() { } } + @Test + public void testNegativeRootLabelKeepsPropertyFilterLocal() { + SchemaManager schema = graph().schema(); + schema.propertyKey("name").asText().create(); + for (String label : new String[]{"indexed", "unindexed"}) { + schema.vertexLabel(label).properties("name").useAutomaticId().create(); + } + schema.indexLabel("indexedByName").onV("indexed").by("name").secondary().create(); + Vertex allowed = graph().addVertex(T.label, "indexed", "name", "alice"); + graph().addVertex(T.label, "unindexed", "name", "alice"); + graph().addVertex(T.label, "indexed", "name", "bob"); + this.commitTx(); + + // Control: the unqualified indexed query really has incomplete coverage. + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().V() + .has("name", "alice").toSet(), error -> + Assert.assertContains("Incomplete index coverage", error.getMessage())); + + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + GraphTraversal query = graph().traversal().V() + .hasLabel(P.neq("unindexed")).has("name", "alice"); + query.asAdmin().applyStrategies(); + HugeGraphStep source = (HugeGraphStep) query.asAdmin().getStartStep(); + // The negative label keeps both predicates local, so the source + // scans vertices without running property-index coverage checks. + Assert.assertTrue(source.getHasContainers().isEmpty()); + Assert.assertEquals(ImmutableSet.of(allowed), query.toSet()); + tracker.assertCounts(0, 0); + } + } + @Test public void testInvalidValuePrecedesPartialCoverageError() { this.addVertices(); From 3b597b76b45f479463d9a2cb42a08842a321d5cb Mon Sep 17 00:00:00 2001 From: contrueCT Date: Mon, 19 Oct 2026 18:06:24 +0800 Subject: [PATCH 8/8] fix(server): complete sub-edge index plans - Resolve query-visible parent indexes for explicit sub-edge labels - Filter sibling edges before limits and preserve page continuation - Close partial plans and unselected joint cursors - Preserve original search failures when cursor cleanup also fails - Add backend counting regressions and update the query contract --- docs/server-runtime.md | 15 +- .../hugegraph/backend/page/IdHolder.java | 18 +- .../backend/tx/GraphIndexTransaction.java | 215 ++++++++----- .../core/SourceIndexCoverageTest.java | 282 ++++++++++++++++++ 4 files changed, 443 insertions(+), 87 deletions(-) diff --git a/docs/server-runtime.md b/docs/server-runtime.md index 28f6ff114d..da09a17c95 100644 --- a/docs/server-runtime.md +++ b/docs/server-runtime.md @@ -257,8 +257,10 @@ For type-compatible values, a global property-index query such as `g.V().has("name", "alice")` or `g.E().has("weight", 0.5)` requires compatible indexes on every visible candidate label. A candidate declares all queried non-OLAP properties; even a label with no records requires coverage. Base edge labels have no -direct records; their indexes can cover sub-labels. Shared OLAP indexes follow the -graph read mode. +direct records; their indexes can cover sub-labels. An explicit sub-label query +also uses a compatible query-visible parent index when no compatible direct index +exists, while still filtering out sibling sub-labels. Shared OLAP indexes follow the graph read +mode. | Candidate state | Query behavior | | --- | --- | @@ -275,10 +277,11 @@ Normal queries exclude hidden labels and labels in `DELETING` or `UNDELETED` status. Internal requests that enable `showHidden` or `showDeleting` include the corresponding labels and still require their indexes. Visibility follows the outer request when a query is split into subqueries. Each branch checks index state and -complete coverage before opening its backend iterators. If a later branch fails -planning, the query closes cursors accumulated from earlier branches and preserves -the original error. This does not provide a snapshot against concurrent schema -changes. +complete coverage before opening its backend iterators. If branch construction +fails, the query closes cursors from both the unfinished branch and earlier +branches, preserving the original error. Joint index planning closes unselected +cursors and transfers the selected cursor to the result. This does not provide a +snapshot against concurrent schema changes. To fix incomplete coverage, specify an indexed label, for example `g.V().hasLabel("person").has("name", "alice")`, or add indexes compatible with the diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java index e6fd34ed64..d5933cb0ff 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/page/IdHolder.java @@ -26,6 +26,7 @@ import org.apache.hugegraph.id.Id; import org.apache.hugegraph.query.ConditionQuery; import org.apache.hugegraph.query.Query; +import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.store.BackendEntry; import org.apache.hugegraph.iterator.CIter; import org.apache.hugegraph.iterator.Metadatable; @@ -231,16 +232,25 @@ public PageIds fetchNext(String page, long batchSize) { @Override public Set all() { + Set ids; try { - Set ids = this.fetcher.apply(this.remaining()); + ids = this.fetcher.apply(this.remaining()); if (this.currentBatch != null) { ids.addAll(this.getFromCurrentBatch(Query.NO_LIMIT).ids()); } this.count += ids.size(); - return ids; - } finally { - this.close(); + } catch (Throwable failure) { + try { + this.close(); + } catch (Throwable closing) { + if (closing != failure) { + failure.addSuppressed(closing); + } + } + throw QueryBatch.propagate(failure); } + this.close(); + return ids; } public PageIds peekNext(long size) { 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 a9eaf5b685..9935644324 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 @@ -43,6 +43,7 @@ import org.apache.hugegraph.backend.page.PageState; import org.apache.hugegraph.backend.page.SortByCountIdHolderList; import org.apache.hugegraph.backend.query.ConditionQueryFlatten; +import org.apache.hugegraph.backend.query.QueryBatch; import org.apache.hugegraph.backend.query.QueryResults; import org.apache.hugegraph.backend.serializer.AbstractSerializer; import org.apache.hugegraph.backend.store.BackendEntry; @@ -416,28 +417,55 @@ private IdHolderList queryByUserprop(ConditionQuery query) { // Do index query IdHolderList holders = new IdHolderList(paging); - for (MatchedIndex index : indexes) { - if (index.containsSearchIndex()) { - // Do search-index query - holders.addAll(this.doSearchIndex(query, index)); - } else { - // Do secondary-index, range-index or shard-index query - IndexQueries queries = constructIndexQueries(query, index); - assert !paging || queries.size() <= 1; - IdHolder holder = this.doSingleOrJointIndex(queries); - holders.add(holder); + try { + for (MatchedIndex index : indexes) { + ConditionQuery indexQuery = prepareIndexQuery(query, index); + if (index.containsSearchIndex()) { + // Do search-index query + holders.addAll(this.doSearchIndex(indexQuery, index)); + } else { + // Do secondary-index, range-index or shard-index query + IndexQueries queries = constructIndexQueries(indexQuery, index); + assert !paging || queries.size() <= 1; + IdHolder holder = this.doSingleOrJointIndex(queries); + holders.add(holder); + } + + /* + * NOTE: need to skip the offset if offset > 0, but can't handle + * it here because the query may a sub-query after flatten, + * so the offset will be handle in QueryList.IndexQuery + * + * TODO: finish early here if records exceeds required limit with + * FixedIdHolder. + */ } + return holders; + } catch (Throwable failure) { + throw closeIndexHolders(failure, holders); + } + } - /* - * NOTE: need to skip the offset if offset > 0, but can't handle - * it here because the query may a sub-query after flatten, - * so the offset will be handle in QueryList.IndexQuery - * - * TODO: finish early here if records exceeds required limit with - * FixedIdHolder. - */ + private static ConditionQuery prepareIndexQuery(ConditionQuery query, MatchedIndex index) { + Id label = query.singleConditionValueOrNull(HugeKeys.LABEL); + if (!query.resultType().isEdge() || label == null || + index.schemaLabel().id().equals(label)) { + return query; + } + // BaseEdge exposes the parent as LABEL and the actual child as SUB_LABEL. + // Filter on the child after fetching from the shared parent index. + ConditionQuery filtered = query.copy(); + filtered.unsetCondition(HugeKeys.LABEL); + filtered.eq(HugeKeys.SUB_LABEL, label); + filtered.optimized(OptimizedType.INDEX_FILTER); + if (!query.paging()) { + filtered.limit(Query.NO_LIMIT); } - return holders; + if (!index.containsSearchIndex()) { + // Search queries install their word-aware filter after rewriting. + filtered.registerResultsFilter(element -> filtered.test(element, null)); + } + return filtered; } @Watched(prefix = "index") @@ -447,18 +475,22 @@ private IdHolderList doSearchIndex(ConditionQuery query, // Sorted by matched count IdHolderList holders = new SortByCountIdHolderList(query.paging()); List flatten = ConditionQueryFlatten.flatten(query); - for (ConditionQuery q : flatten) { - if (!q.noLimit() && flatten.size() > 1) { - // Increase limit for union operation - increaseLimit(q); + try { + for (ConditionQuery q : flatten) { + if (!q.noLimit() && flatten.size() > 1) { + // Increase limit for union operation + increaseLimit(q); + } + IndexQueries queries = constructIndexQueries(q, index); + assert !query.paging() || queries.size() <= 1; + IdHolder holder = this.doSingleOrJointIndex(queries); + // NOTE: ids will be merged into one IdHolder if not in paging + holders.add(holder); } - IndexQueries queries = constructIndexQueries(q, index); - assert !query.paging() || queries.size() <= 1; - IdHolder holder = this.doSingleOrJointIndex(queries); - // NOTE: ids will be merged into one IdHolder if not in paging - holders.add(holder); + return holders; + } catch (Throwable failure) { + throw closeIndexHolders(failure, holders); } - return holders; } @Watched(prefix = "index") @@ -490,61 +522,79 @@ private IdHolder doJointIndex(IndexQueries queries) { Set intersectIds = null; boolean filtering = false; IdHolder resultHolder = null; - for (Map.Entry e : queries.entrySet()) { - IndexLabel indexLabel = e.getKey(); - ConditionQuery query = e.getValue(); - assert !query.paging(); - if (!query.noLimit() && queries.size() > 1) { - // Unset limit for intersection operation - query.limit(Query.NO_LIMIT); - } - /* - * Try to query by joint indexes: - * 1 If there is any index exceeded the threshold, transform into - * partial index query, then filter after back-table. - * 1.1 Return the holder of the first index that not exceeded the - * threshold if there exists one index, this holder will be used - * as the only query condition. - * 1.2 Return the holder of the first index if all indexes exceeded - * the threshold. - * 2 Else intersect holders for all indexes, and return intersection - * ids of all indexes. - */ - IdHolder holder = this.doIndexQuery(indexLabel, query); - if (resultHolder == null) { - resultHolder = holder; - this.storeSelectedIndexField(indexLabel, query); - } - assert this.indexIntersectThresh > 0; // default value is 1000 - Set ids = ((BatchIdHolder) holder).peekNext( - this.indexIntersectThresh).ids(); - if (ids.size() >= this.indexIntersectThresh) { - // Transform into filtering - filtering = true; - query.optimized(OptimizedType.INDEX_FILTER); - } else if (filtering) { - assert ids.size() < this.indexIntersectThresh; - resultHolder = holder; - this.storeSelectedIndexField(indexLabel, query); - break; - } else { - if (intersectIds == null) { - intersectIds = ids; - } else { - CollectionUtil.intersectWithModify(intersectIds, ids); + List holders = new ArrayList<>(queries.size()); + try { + for (Map.Entry e : queries.entrySet()) { + IndexLabel indexLabel = e.getKey(); + ConditionQuery query = e.getValue(); + assert !query.paging(); + if (!query.noLimit() && queries.size() > 1) { + // Unset limit for intersection operation + query.limit(Query.NO_LIMIT); + } + /* + * Try to query by joint indexes: + * 1 If there is any index exceeded the threshold, transform into + * partial index query, then filter after back-table. + * 1.1 Return the holder of the first index that not exceeded the + * threshold if there exists one index, this holder will be used + * as the only query condition. + * 1.2 Return the holder of the first index if all indexes exceeded + * the threshold. + * 2 Else intersect holders for all indexes, and return intersection + * ids of all indexes. + */ + IdHolder holder = this.doIndexQuery(indexLabel, query); + holders.add(holder); + if (resultHolder == null) { + resultHolder = holder; + this.storeSelectedIndexField(indexLabel, query); } - if (intersectIds.isEmpty()) { + assert this.indexIntersectThresh > 0; // default value is 1000 + Set ids = ((BatchIdHolder) holder).peekNext( + this.indexIntersectThresh).ids(); + if (ids.size() >= this.indexIntersectThresh) { + // Transform into filtering + filtering = true; + query.optimized(OptimizedType.INDEX_FILTER); + } else if (filtering) { + assert ids.size() < this.indexIntersectThresh; + resultHolder = holder; + this.storeSelectedIndexField(indexLabel, query); break; + } else { + if (intersectIds == null) { + intersectIds = ids; + } else { + CollectionUtil.intersectWithModify(intersectIds, ids); + } + if (intersectIds.isEmpty()) { + break; + } } } + + IdHolder result = filtering ? resultHolder : + new FixedIdHolder(queries.asJointQuery(), intersectIds); + // Transfer only the selected holder. Other lazy cursors may still + // be open when an index exceeded the intersection threshold. + QueryBatch.closeAll(holders.stream().filter(holder -> holder != result).toArray()); + return result; + } catch (Throwable failure) { + throw closeIndexHolders(failure, holders); } + } - if (filtering) { - return resultHolder; - } else { - assert intersectIds != null; - return new FixedIdHolder(queries.asJointQuery(), intersectIds); + private static RuntimeException closeIndexHolders(Throwable failure, + Collection holders) { + try { + QueryBatch.closeAll(holders.toArray()); + } catch (Throwable closing) { + if (closing != failure) { + failure.addSuppressed(closing); + } } + return QueryBatch.propagate(failure); } private void storeSelectedIndexField(IndexLabel indexLabel, @@ -703,6 +753,12 @@ private List querySchemaLabels(ConditionQuery query) { query.resultType())); } schemaLabels = ImmutableList.of(schemaLabel); + if (schemaLabel instanceof EdgeLabel && ((EdgeLabel) schemaLabel).hasFather()) { + // Include the parent in visibility filtering and index matching; + // the original query label still filters out sibling edges. + EdgeLabel father = schema.getEdgeLabel(((EdgeLabel) schemaLabel).fatherId()); + schemaLabels = ImmutableList.of(schemaLabel, father); + } } else { // Query doesn't have LABEL condition or it doesn't resolve // to a single label, so keep the conservative fallback. @@ -734,6 +790,11 @@ private Set collectMatchedIndexes(ConditionQuery query, MatchedIndex index = this.collectMatchedIndex(schemaLabel, query); if (index != null) { matchedIndexes.add(index); + // Prefer a compatible direct index for an explicit sub-label. + // Its parent, when present, follows it in querySchemaLabels(). + if (schemaLabel.id().equals(query.singleConditionValueOrNull(HugeKeys.LABEL))) { + break; + } } } return matchedIndexes; diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java index 11cc517ba0..f132091f1c 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/SourceIndexCoverageTest.java @@ -24,6 +24,7 @@ import java.util.Set; import org.apache.hugegraph.backend.page.PageInfo; +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.backend.tx.GraphIndexTransaction; @@ -42,6 +43,7 @@ import org.apache.hugegraph.traversal.optimize.Text; import org.apache.hugegraph.type.HugeType; import org.apache.hugegraph.type.define.GraphReadMode; +import org.apache.hugegraph.type.define.HugeKeys; import org.apache.hugegraph.type.define.IdStrategy; import org.apache.hugegraph.type.define.SchemaStatus; import org.apache.hugegraph.type.define.WriteType; @@ -215,6 +217,269 @@ public void testCompleteOrPlanKeepsCursorsUntilResultClose() { } } + @Test + public void testLabeledSubEdgeUsesParentIndex() { + Assume.assumeTrue(storeFeatures().supportsFatherAndSubEdgeLabel()); + SchemaManager schema = graph().schema(); + List edges = this.addSubEdges(); + Edge expected = edges.get(0); + Edge sibling = edges.get(1); + schema.indexLabel("parentEdgeByName").onE("parentEdge") + .by("coverageName").secondary().create(); + Assert.assertEquals(2, + graph().traversal().E().has("coverageName", "same").toList().size()); + Assert.assertEquals(ImmutableList.of(sibling), graph().traversal().E() + .hasLabel("subB").has("coverageName", "same").limit(1).toList()); + Assert.assertEquals(ImmutableSet.of(expected), graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet()); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").limit(1).toList()); + if (storeFeatures().supportsQueryByPage()) { + for (Edge edge : edges) { + Set actual = new HashSet<>(); + String page = ""; + int pages = 0; + do { + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.eq(HugeKeys.LABEL, graph().edgeLabel(edge.label()).id()); + query.limit(1); + query.page(page); + Iterator results = graph().edges(query); + try { + results.forEachRemaining(actual::add); + page = PageInfo.pageInfo(results); + } finally { + CloseableIterator.closeIterator(results); + } + Assert.assertTrue(++pages < 10); + } while (page != null && !page.isEmpty()); + Assert.assertEquals(ImmutableSet.of(edge), actual); + } + } + + SchemaLabel parentLabel = schema.getEdgeLabel("parentEdge"); + try { + this.params().schemaTransaction().updateSchemaStatus(parentLabel, SchemaStatus.DELETING); + Assert.assertThrows(NoIndexException.class, () -> graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet()); + ConditionQuery query = this.nameQuery(HugeType.EDGE); + query.eq(HugeKeys.LABEL, graph().edgeLabel("subA").id()); + query.showDeleting(true); + Assert.assertEquals(ImmutableSet.of(expected), ImmutableSet.copyOf(graph().edges(query))); + } finally { + this.params().schemaTransaction().updateSchemaStatus(parentLabel, SchemaStatus.CREATED); + } + + IndexLabel parent = schema.getIndexLabel("parentEdgeByName"); + try { + this.params().schemaTransaction().updateSchemaStatus(parent, SchemaStatus.REBUILDING); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Assert.assertThrows(IllegalArgumentException.class, () -> graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet(), error -> + Assert.assertContains("REBUILDING", error.getMessage())); + tracker.assertCounts(0, 0); + } + schema.indexLabel("subAByName").onE("subA").by("coverageName").secondary().create(); + // A compatible direct index is preferred over the parent fallback. + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Assert.assertEquals(ImmutableSet.of(expected), graph().traversal().E() + .hasLabel("subA").has("coverageName", "same").toSet()); + tracker.assertCounts(1, 1); + } + } finally { + this.params().schemaTransaction().updateSchemaStatus(parent, SchemaStatus.CREATED); + } + } + + @Test + public void testLabeledSubEdgeUsesParentRangeJointAndSearchIndexes() { + Assume.assumeTrue(storeFeatures().supportsFatherAndSubEdgeLabel()); + SchemaManager schema = graph().schema(); + Edge expected = this.addSubEdges().get(1); + schema.indexLabel("parentByScore").onE("parentEdge").by("coverageScore").range().create(); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subB").has("coverageScore", P.gte(1)).limit(1).toList()); + schema.indexLabel("parentByName").onE("parentEdge").by("coverageName").secondary().create(); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subB").has("coverageName", "same") + .has("coverageScore", P.gte(1)).limit(1).toList()); + schema.indexLabel("parentByName").remove(); + schema.indexLabel("parentSearchByName").onE("parentEdge").by("coverageName").search().create(); + Assert.assertEquals(ImmutableList.of(expected), graph().traversal().E() + .hasLabel("subB").has("coverageName", Text.contains("same")).limit(1).toList()); + } + + private List addSubEdges() { + SchemaManager schema = graph().schema(); + schema.edgeLabel("parentEdge").asBase().properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + for (String label : new String[]{"subA", "subB"}) { + schema.edgeLabel(label).withBase("parentEdge").link("coverageA", "coverageB") + .properties("coverageName", "coverageScore") + .multiTimes().sortKeys("coverageScore").create(); + } + Set vertices = this.addVertices(); + Vertex a = vertices.stream().filter(v -> v.label().equals("coverageA")).findFirst().get(); + Vertex b = vertices.stream().filter(v -> v.label().equals("coverageB")).findFirst().get(); + Edge first = a.addEdge("subA", b, "coverageName", "same", "coverageScore", 1); + Edge second = a.addEdge("subB", b, "coverageName", "same", "coverageScore", 2); + this.commitTx(); + return ImmutableList.of(first, second); + } + + @Test + public void testSingleBranchFailureClosesPartialCursors() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + this.addVertices(); + RuntimeException injected = new IllegalStateException("second index query failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(null, injected, 2)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(this.nameQuery(HugeType.VERTEX))); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + } + } + + @Test + public void testJointBranchFailureClosesPartialCursors() { + this.prepareJointIndexes(); + ConditionQuery query = this.jointQuery(); + RuntimeException injected = new IllegalStateException("second joint index query failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(null, injected, 2)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(query)); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + } + } + + @Test + public void testSingleBranchFailurePreservesErrorAndClosesAllCursors() { + this.nameIndex("coverageA"); + this.nameIndex("coverageB"); + graph().schema().vertexLabel("coverageC").properties("coverageName") + .useAutomaticId().create(); + this.nameIndex("coverageC"); + this.addVertices(); + RuntimeException injected = new IllegalStateException("third index query failed"); + RuntimeException closing = new IllegalStateException("first index cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing, injected, 3)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(this.nameQuery(HugeType.VERTEX))); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(2, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } + } + + @Test + public void testJointPlanClosesUnselectedCursorsAndTransfersSelected() { + Set expected = this.prepareJointIndexes(); + ConditionQuery query = this.jointQuery(); + try (IndexCursorTracker tracker = new IndexCursorTracker(null)) { + Iterator results = graph().vertices(query); + try { + Assert.assertEquals(2, tracker.cursors.size()); + Assert.assertEquals(1, tracker.cursors.stream().mapToInt(cursor -> cursor.closes).sum()); + Assert.assertEquals(expected, ImmutableSet.copyOf(results)); + } finally { + CloseableIterator.closeIterator(results); + } + tracker.assertCounts(2, 1); + } + } + + @Test + public void testJointReadFailureClosesAllCursorsAndPreservesError() { + this.prepareJointIndexes(); + RuntimeException injected = new IllegalStateException("second joint cursor read failed"); + RuntimeException closing = new IllegalStateException("first joint cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing)) { + tracker.failOnRead = 2; + tracker.readFailure = injected; + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().vertices(this.jointQuery())); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(2, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } + } + + @Test + public void testSearchReadFailureClosesCursorAndPreservesError() { + graph().schema().indexLabel("coverageASearch").onV("coverageA") + .by("coverageBody").search().create(); + this.addVertices(); + RuntimeException injected = new IllegalStateException("search index cursor read failed"); + RuntimeException closing = new IllegalStateException("search index cursor close failed"); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing)) { + tracker.failOnRead = 1; + tracker.readFailure = injected; + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().traversal().V().hasLabel("coverageA") + .has("coverageBody", Text.contains("gold")).toSet()); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } + } + + @Test + public void testSearchDecodeFailureClosesCursorAndPreservesError() { + graph().schema().indexLabel("coverageASearch").onV("coverageA") + .by("coverageBody").search().create(); + this.addVertices(); + RuntimeException injected = new IllegalStateException("search index decode failed"); + RuntimeException closing = new IllegalStateException("search index cursor close failed"); + GraphIndexTransaction indexTx = Whitebox.getInternalState(params().graphTransaction(), "indexTx"); + AbstractSerializer original = Whitebox.getInternalState(indexTx, "serializer"); + AbstractSerializer observed = Mockito.mock(AbstractSerializer.class, + AdditionalAnswers.delegatesTo(original)); + Mockito.doAnswer(invocation -> { + // Decode a real backend entry before failing outside the iterator wrapper. + original.readIndex(invocation.getArgument(0), invocation.getArgument(1), + invocation.getArgument(2)); + throw injected; + }).when(observed).readIndex(Mockito.any(), Mockito.any(), Mockito.any()); + Whitebox.setInternalState(indexTx, "serializer", observed); + try (IndexCursorTracker tracker = new IndexCursorTracker(closing)) { + RuntimeException failure = Assert.assertThrows(RuntimeException.class, + () -> graph().traversal().V().hasLabel("coverageA") + .has("coverageBody", Text.contains("gold")).toSet()); + Assert.assertSame(injected, Throwables.getRootCause(failure)); + tracker.assertCounts(1, 1); + Assert.assertEquals(1, injected.getSuppressed().length); + Assert.assertSame(closing, Throwables.getRootCause(injected.getSuppressed()[0])); + } finally { + Whitebox.setInternalState(indexTx, "serializer", original); + } + } + + private Set prepareJointIndexes() { + this.nameIndex("coverageA"); + graph().schema().indexLabel("coverageAByScore").onV("coverageA") + .by("coverageScore").secondary().create(); + // Exceed the fixture's intersection threshold of 2 to retain lazy cursors. + Set expected = new HashSet<>(); + for (int i = 0; i < 3; i++) { + expected.add(graph().addVertex(T.label, "coverageA", "coverageName", "same", + "coverageScore", 10, "coverageBody", "gold")); + } + this.commitTx(); + return expected; + } + + private ConditionQuery jointQuery() { + ConditionQuery query = this.nameQuery(HugeType.VERTEX); + query.eq(HugeKeys.LABEL, graph().vertexLabel("coverageA").id()); + query.query(Condition.eq(graph().propertyKey("coverageScore").id(), 10)); + return query; + } + private void prepareOrIndexes(boolean complete) { this.nameIndex("coverageA"); this.nameIndex("coverageB"); @@ -239,18 +504,31 @@ private final class IndexCursorTracker implements AutoCloseable { private final GraphIndexTransaction indexTx; private final BackendStore original; private final List cursors = new ArrayList<>(); + private int failOnRead; + private RuntimeException readFailure; private IndexCursorTracker(RuntimeException closeFailure) { + this(closeFailure, null, 0); + } + + private IndexCursorTracker(RuntimeException closeFailure, RuntimeException queryFailure, + int failOnQuery) { this.indexTx = Whitebox.getInternalState(params().graphTransaction(), "indexTx"); this.original = Whitebox.getInternalState(this.indexTx, "store"); BackendStore observed = Mockito.mock(BackendStore.class, AdditionalAnswers.delegatesTo(this.original)); Mockito.doAnswer(invocation -> { + if (queryFailure != null && this.cursors.size() + 1 == failOnQuery) { + throw queryFailure; + } // Keep real index matching, validation, serialization and backend queries. Iterator entries = this.original.query((Query) invocation.getArgument(0)); CountingIndexCursor cursor = new CountingIndexCursor(entries, this.cursors.isEmpty() ? closeFailure : null); this.cursors.add(cursor); + if (this.cursors.size() == this.failOnRead) { + cursor.readFailure = this.readFailure; + } return cursor; }).when(observed).query(Mockito.any(Query.class)); Whitebox.setInternalState(this.indexTx, "store", observed); @@ -280,6 +558,7 @@ private static final class CountingIndexCursor implements CloseableIterator entries; private final RuntimeException closeFailure; private int closes; + private RuntimeException readFailure; private CountingIndexCursor(Iterator entries, RuntimeException closeFailure) { this.entries = entries; @@ -288,6 +567,9 @@ private CountingIndexCursor(Iterator entries, RuntimeException clo @Override public boolean hasNext() { + if (this.readFailure != null) { + throw this.readFailure; + } return this.entries.hasNext(); }