From a10a42e2172f95881f06161a4de0f98ce292d6b1 Mon Sep 17 00:00:00 2001 From: Manfred Baedke Date: Wed, 26 Nov 2025 14:54:53 +0100 Subject: [PATCH 1/5] OAK-11697: Indexing / query limit / traversal error: improve diagnostics Extended logging and error message. --- .../java/org/apache/jackrabbit/oak/query/QueryImpl.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java index 3f08b4fae4e..d9065919fb4 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java @@ -1267,6 +1267,14 @@ public void verifyNotPotentiallySlow() { } String caller = IndexUtils.getCaller(settings.getIgnoredClassNamesInCallTrace()); String message = "Traversal query (query without index): " + statement + "; called by " + caller + "; consider creating an index"; + if (traversal != Traversal.OK) { + String plan = getPlan(); + List indexList = context.getIndexProvider().getQueryIndexes(context.getBaseState()) + .stream() + .sorted(MINIMAL_COST_ORDERING) + .collect(Collectors.toList()); + message += "\n" + plan + "\n" + "Available indexes at the time of query execution:" + "\n" + indexList; + } switch (traversal) { case DEFAULT: // not possible (changed to either FAIL or WARN above) From 72829b70b843fbec636b5fccc2f7faee21ab665c Mon Sep 17 00:00:00 2001 From: Manfred Baedke Date: Wed, 26 Nov 2025 15:33:20 +0100 Subject: [PATCH 2/5] OAK-11697: Indexing / query limit / traversal error: improve diagnostics Minor optimization. --- .../main/java/org/apache/jackrabbit/oak/query/QueryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java index d9065919fb4..7a9fdb77398 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java @@ -1267,7 +1267,7 @@ public void verifyNotPotentiallySlow() { } String caller = IndexUtils.getCaller(settings.getIgnoredClassNamesInCallTrace()); String message = "Traversal query (query without index): " + statement + "; called by " + caller + "; consider creating an index"; - if (traversal != Traversal.OK) { + if (traversal == Traversal.FAIL || traversal == Traversal.WARN && !potentiallySlowTraversalQueryLogged) { String plan = getPlan(); List indexList = context.getIndexProvider().getQueryIndexes(context.getBaseState()) .stream() From f38fee980567e060686cf3636b7791604fff0f5d Mon Sep 17 00:00:00 2001 From: Manfred Baedke Date: Wed, 26 Nov 2025 16:12:27 +0100 Subject: [PATCH 3/5] OAK-11697: Indexing / query limit / traversal error: improve diagnostics Minor optimization. --- .../org/apache/jackrabbit/oak/query/QueryImpl.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java index 7a9fdb77398..b2056519f2f 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java @@ -190,6 +190,8 @@ public class QueryImpl implements Query { private boolean potentiallySlowTraversalQuery; + private List queryIndexes; + QueryImpl(String statement, SourceImpl source, ConstraintImpl constraint, ColumnImpl[] columns, NamePathMapper mapper, QueryEngineSettings settings, QueryExecutionStats stats) { @@ -1085,7 +1087,7 @@ private SelectorExecutionPlan getBestSelectorExecutionPlan( // Sort the indexes according to their minimum cost to be able to skip the remaining indexes if the cost of the // current index is below the minimum cost of the next index. - List queryIndexes = indexProvider.getQueryIndexes(rootState).stream() + queryIndexes = indexProvider.getQueryIndexes(rootState).stream() .sorted(MINIMAL_COST_ORDERING).collect(Collectors.toList()); List sortOrder = getSortOrder(filter); for (int i = 0; i < queryIndexes.size(); i++) { @@ -1268,12 +1270,7 @@ public void verifyNotPotentiallySlow() { String caller = IndexUtils.getCaller(settings.getIgnoredClassNamesInCallTrace()); String message = "Traversal query (query without index): " + statement + "; called by " + caller + "; consider creating an index"; if (traversal == Traversal.FAIL || traversal == Traversal.WARN && !potentiallySlowTraversalQueryLogged) { - String plan = getPlan(); - List indexList = context.getIndexProvider().getQueryIndexes(context.getBaseState()) - .stream() - .sorted(MINIMAL_COST_ORDERING) - .collect(Collectors.toList()); - message += "\n" + plan + "\n" + "Available indexes at the time of query execution:" + "\n" + indexList; + message += "\n" + getPlan() + "\n" + "Available indexes at the time of query execution:" + "\n" + queryIndexes; } switch (traversal) { case DEFAULT: From 44f7b967366319a990b75b0c84d2a4691bbae0c0 Mon Sep 17 00:00:00 2001 From: Manfred Baedke Date: Thu, 27 Nov 2025 17:12:02 +0100 Subject: [PATCH 4/5] OAK-11697: Indexing / query limit / traversal error: improve diagnostics Re-formatted diagnostic text, added test case. --- .../jackrabbit/oak/query/QueryImpl.java | 2 +- .../jackrabbit/oak/jcr/query/QueryTest.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java index b2056519f2f..be8492f861a 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java @@ -1270,7 +1270,7 @@ public void verifyNotPotentiallySlow() { String caller = IndexUtils.getCaller(settings.getIgnoredClassNamesInCallTrace()); String message = "Traversal query (query without index): " + statement + "; called by " + caller + "; consider creating an index"; if (traversal == Traversal.FAIL || traversal == Traversal.WARN && !potentiallySlowTraversalQueryLogged) { - message += "\n" + getPlan() + "\n" + "Available indexes at the time of query execution:" + "\n" + queryIndexes; + message += "\n\nAvailable indexes at the time of query execution:\n" + queryIndexes + "\n\nExecution plan:\n" + getPlan(); } switch (traversal) { case DEFAULT: diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java index 04b383957c9..29b863c0b7e 100644 --- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java @@ -138,6 +138,25 @@ public void testSettingsOverride() throws Exception { assertEquals(90, count); } + @Test + public void traversalExtendedDiagnosis() throws Exception { + Session session = getAdminSession(); + QueryManager qm = session.getWorkspace().getQueryManager(); + + try { + qm.createQuery("select * from [nt:base] where [x] = 1 or [y] = 2 option(traversal fail)", Query.JCR_SQL2).execute(); + fail("traversing query should not succeed"); + } catch (RepositoryException e) { + String message = e.getMessage(); + assertTrue(message.contains("Traversal query (query without index)")); + assertTrue(message.contains("Available indexes at the time of query execution")); + assertTrue(message.contains("ReferenceIndex")); + assertTrue(message.contains("NodeTypeIndex")); + assertTrue(message.contains("Execution plan")); + assertTrue(message.contains("[nt:base] as [nt:base] /* traverse")); + } + } + @Test public void traversalOption() throws Exception { Session session = getAdminSession(); From 85366c9dfe05705bad62439253c84d95d7be079d Mon Sep 17 00:00:00 2001 From: Manfred Baedke Date: Thu, 4 Dec 2025 17:14:08 +0100 Subject: [PATCH 5/5] OAK-11697: Indexing / query limit / traversal error: improve diagnostics new approach --- .../jackrabbit/oak/query/QueryImpl.java | 29 ++++++++++++++--- .../jackrabbit/oak/jcr/query/QueryTest.java | 31 +++++++++++++++---- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java index be8492f861a..f951bfb7718 100644 --- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryImpl.java @@ -14,6 +14,10 @@ package org.apache.jackrabbit.oak.query; import static java.util.Objects.requireNonNull; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME; import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference; import java.math.BigInteger; @@ -32,9 +36,11 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.apache.jackrabbit.oak.api.PropertyState; import org.apache.jackrabbit.oak.api.PropertyValue; import org.apache.jackrabbit.oak.api.Result.SizePrecision; import org.apache.jackrabbit.oak.api.Tree; +import org.apache.jackrabbit.oak.api.Type; import org.apache.jackrabbit.oak.namepath.JcrPathParser; import org.apache.jackrabbit.oak.namepath.NamePathMapper; import org.apache.jackrabbit.oak.plugins.index.IndexUtils; @@ -190,8 +196,6 @@ public class QueryImpl implements Query { private boolean potentiallySlowTraversalQuery; - private List queryIndexes; - QueryImpl(String statement, SourceImpl source, ConstraintImpl constraint, ColumnImpl[] columns, NamePathMapper mapper, QueryEngineSettings settings, QueryExecutionStats stats) { @@ -1087,7 +1091,7 @@ private SelectorExecutionPlan getBestSelectorExecutionPlan( // Sort the indexes according to their minimum cost to be able to skip the remaining indexes if the cost of the // current index is below the minimum cost of the next index. - queryIndexes = indexProvider.getQueryIndexes(rootState).stream() + List queryIndexes = indexProvider.getQueryIndexes(rootState).stream() .sorted(MINIMAL_COST_ORDERING).collect(Collectors.toList()); List sortOrder = getSortOrder(filter); for (int i = 0; i < queryIndexes.size(); i++) { @@ -1270,7 +1274,24 @@ public void verifyNotPotentiallySlow() { String caller = IndexUtils.getCaller(settings.getIgnoredClassNamesInCallTrace()); String message = "Traversal query (query without index): " + statement + "; called by " + caller + "; consider creating an index"; if (traversal == Traversal.FAIL || traversal == Traversal.WARN && !potentiallySlowTraversalQueryLogged) { - message += "\n\nAvailable indexes at the time of query execution:\n" + queryIndexes + "\n\nExecution plan:\n" + getPlan(); + HashSet reindex = new HashSet<>(); + Iterable indexes = context.getRoot().getTree("/" + INDEX_DEFINITIONS_NAME).getChildren(); + for (Tree index : indexes) { + String name = index.getName(); + PropertyState primaryType = index.getProperty(JCR_PRIMARYTYPE); + if (primaryType != null && INDEX_DEFINITIONS_NODE_TYPE.equals(primaryType.getValue(Type.STRING))) { + PropertyState reindexProp = index.getProperty(REINDEX_PROPERTY_NAME); + if (reindexProp != null && reindexProp.getValue(Type.BOOLEAN)) { + reindex.add(name); + } + } + } + message += "\n\nExecution plan:\n" + getPlan(); + if (!reindex.isEmpty()) { + String reindexNames = reindex.stream().map(name -> name + ",").collect(Collectors.joining()); + message += "\n\nNote that the following indexes were unavailable because of re-indexing:\n" + + reindexNames.substring(0, reindexNames.length() - 1); + } } switch (traversal) { case DEFAULT: diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java index 29b863c0b7e..e8d7e154e85 100644 --- a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryTest.java @@ -18,6 +18,10 @@ */ package org.apache.jackrabbit.oak.jcr.query; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME; +import static org.apache.jackrabbit.oak.plugins.index.IndexUtils.createIndexDefinition; +import static org.apache.jackrabbit.oak.spi.commit.CommitInfo.EMPTY; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; @@ -41,6 +45,7 @@ import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.ValueFactory; +import javax.jcr.Workspace; import javax.jcr.nodetype.NodeTypeManager; import javax.jcr.query.InvalidQueryException; import javax.jcr.query.Query; @@ -60,6 +65,8 @@ import org.apache.jackrabbit.oak.commons.json.JsopTokenizer; import org.apache.jackrabbit.oak.fixture.NodeStoreFixture; import org.apache.jackrabbit.oak.jcr.AbstractRepositoryTest; +import org.apache.jackrabbit.oak.spi.commit.EmptyHook; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.junit.Ignore; import org.junit.Test; @@ -141,19 +148,31 @@ public void testSettingsOverride() throws Exception { @Test public void traversalExtendedDiagnosis() throws Exception { Session session = getAdminSession(); - QueryManager qm = session.getWorkspace().getQueryManager(); - + Workspace workspace = session.getWorkspace(); + NodeBuilder rootBuilder = getNodeStore().getRoot().builder(); + createIndexDefinition( + rootBuilder.child(INDEX_DEFINITIONS_NAME), "foo", true, false, + Set.of("foo"), null); + getNodeStore().merge(rootBuilder, EmptyHook.INSTANCE, EMPTY); + session.refresh(true); + rootBuilder.child("a").setProperty("foo", "abc"); + //set reindex=true without a CommitHook to simulate an ongoing re-indexing process. + rootBuilder.child(INDEX_DEFINITIONS_NAME).child("foo").setProperty(REINDEX_PROPERTY_NAME, true); + getNodeStore().merge(rootBuilder, EmptyHook.INSTANCE, EMPTY); + session.refresh(true); + Node foo = session.getNode("/" + INDEX_DEFINITIONS_NAME).getNode("foo"); + assertTrue(foo.getProperty(REINDEX_PROPERTY_NAME).getBoolean()); + Query query = workspace.getQueryManager().createQuery("select * from [nt:base] where [x] = 1 or [y] = 2 option(traversal fail)", Query.JCR_SQL2); try { - qm.createQuery("select * from [nt:base] where [x] = 1 or [y] = 2 option(traversal fail)", Query.JCR_SQL2).execute(); + query.execute(); fail("traversing query should not succeed"); } catch (RepositoryException e) { String message = e.getMessage(); assertTrue(message.contains("Traversal query (query without index)")); - assertTrue(message.contains("Available indexes at the time of query execution")); - assertTrue(message.contains("ReferenceIndex")); - assertTrue(message.contains("NodeTypeIndex")); assertTrue(message.contains("Execution plan")); assertTrue(message.contains("[nt:base] as [nt:base] /* traverse")); + assertTrue(message.contains("Note that the following indexes were unavailable because of re-indexing")); + assertTrue(message.contains("foo")); } }