This is an automated email from the ASF dual-hosted git repository.

thomasm pushed a commit to branch OAK-12057
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/OAK-12057 by this push:
     new 3f7e4cdc80 OAK-12057 - Select same plan with or without LIMIT option 
(#2722)
3f7e4cdc80 is described below

commit 3f7e4cdc801336d188f9b8d3ad917b1301747a8a
Author: Benjamin Habegger <[email protected]>
AuthorDate: Wed Feb 4 13:37:04 2026 +0100

    OAK-12057 - Select same plan with or without LIMIT option (#2722)
    
    * Simplify cost evaluation test
    
    * OAK-12057 - Select same plan with or without LIMIT option
---
 .../org/apache/jackrabbit/oak/query/QueryImpl.java |   8 --
 .../oak/query/SQL2OptimiseQueryTest.java           |  10 +-
 .../jackrabbit/oak/query/SQL2ParserTest.java       |  81 ++++++++++++++-
 .../jackrabbit/oak/IndexCostEvaluationTest.java    | 110 +++++++++------------
 .../jackrabbit/oak/spi/query/QueryIndex.java       |   4 -
 5 files changed, 127 insertions(+), 86 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 3f08b4fae4..cf9430d655 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
@@ -1081,8 +1081,6 @@ public class QueryImpl implements Query {
         double almostBestCost = Double.POSITIVE_INFINITY;
         IndexPlan almostBestPlan = null;
 
-        long maxEntryCount = saturatedAdd(offset.orElse(0L), 
limit.orElse(Long.MAX_VALUE));
-
         // 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<? extends QueryIndex> queryIndexes = 
indexProvider.getQueryIndexes(rootState).stream()
@@ -1117,12 +1115,6 @@ public class QueryImpl implements Query {
                     if (p.getSupportsPathRestriction()) {
                         entryCount = scaleEntryCount(rootState, filter, 
entryCount);
                     }
-                    if (sortOrder == null || p.getSortOrder() != null) {
-                        // if the query is unordered, or
-                        // if the query contains "order by" and the index can 
sort on that,
-                        // then we don't need to read all entries from the 
index
-                        entryCount = Math.min(maxEntryCount, entryCount);
-                    }
                     double c = p.getCostPerExecution() + entryCount * 
p.getCostPerEntry();
 
                     if (LOG.isDebugEnabled()) {
diff --git 
a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java
 
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java
index b42594ec93..ec2d4978d1 100644
--- 
a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java
+++ 
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2OptimiseQueryTest.java
@@ -36,8 +36,6 @@ import static org.junit.Assert.assertTrue;
 
 import java.text.ParseException;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 import javax.jcr.RepositoryException;
 
@@ -88,7 +86,7 @@ public class SQL2OptimiseQueryTest extends  AbstractQueryTest 
{
                 + "  AND ((CONTAINS(*, '10') AND ([jcr:uuid] LIKE '11' OR 
[jcr:uuid] LIKE '12'))\n"
                 + "       OR (CONTAINS(*, '13') AND ([jcr:uuid] LIKE '14' OR 
[jcr:uuid] LIKE '15')))";
         SQL2Parser parser = SQL2ParserTest.createTestSQL2Parser(
-                getMappings(), getNodeTypes(), qeSettings);
+                getMappings(), getNodeTypes());
         Query original;
         original = parser.parse(query, false);
         assertNotNull(original);
@@ -219,7 +217,7 @@ public class SQL2OptimiseQueryTest extends  
AbstractQueryTest {
     @Test
     public void optimise() throws ParseException {
         SQL2Parser parser = SQL2ParserTest.createTestSQL2Parser(
-                getMappings(), getNodeTypes(), qeSettings);
+                getMappings(), getNodeTypes());
         String statement;
         Query original, optimised;
 
@@ -294,7 +292,7 @@ public class SQL2OptimiseQueryTest extends  
AbstractQueryTest {
     
     private void optimiseAndOrAnd(String statement, String expected) throws 
ParseException {
         SQL2Parser parser = SQL2ParserTest.createTestSQL2Parser(
-                getMappings(), getNodeTypes(), qeSettings);
+                getMappings(), getNodeTypes());
         Query original;
         original = parser.parse(statement, false);
         assertNotNull(original);
@@ -310,7 +308,7 @@ public class SQL2OptimiseQueryTest extends  
AbstractQueryTest {
     @Test
     public void optimizeKeepsQueryOptions() throws ParseException {
         SQL2Parser parser = SQL2ParserTest.createTestSQL2Parser(
-                getMappings(), getNodeTypes(), qeSettings);
+                getMappings(), getNodeTypes());
         Query original;
         String statement = "select * from [nt:unstructured] as [c] " + 
                 "where [a]=1 or [b]=2 option(index tag x)";
diff --git 
a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java 
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java
index 20f96dca09..2709a2cd0b 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/SQL2ParserTest.java
@@ -20,13 +20,22 @@ import static 
org.apache.jackrabbit.oak.InitialContentHelper.INITIAL_CONTENT;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
 
 import java.text.ParseException;
+import java.util.List;
 
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.query.ast.NodeTypeInfoProvider;
 import org.apache.jackrabbit.oak.query.stats.QueryStatsData;
 import org.apache.jackrabbit.oak.query.xpath.XPathToSQL2Converter;
+import org.apache.jackrabbit.oak.spi.query.QueryIndex;
+import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.jetbrains.annotations.NotNull;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -40,14 +49,12 @@ public class SQL2ParserTest {
     private static final SQL2Parser p = createTestSQL2Parser();
 
     public static SQL2Parser createTestSQL2Parser() {
-        return createTestSQL2Parser(NamePathMapper.DEFAULT, nodeTypes, new 
QueryEngineSettings());
+        return createTestSQL2Parser(NamePathMapper.DEFAULT, nodeTypes);
     }
 
-    public static SQL2Parser createTestSQL2Parser(NamePathMapper mappings, 
NodeTypeInfoProvider nodeTypes2,
-            QueryEngineSettings qeSettings) {
+    public static SQL2Parser createTestSQL2Parser(NamePathMapper mappings, 
NodeTypeInfoProvider nodeTypes2) {
         QueryStatsData data = new QueryStatsData("", "");
-        return new SQL2Parser(mappings, nodeTypes2, new QueryEngineSettings(),
-                data.new QueryExecutionStats());
+        return new SQL2Parser(mappings, nodeTypes2, new QueryEngineSettings(), 
data.new QueryExecutionStats());
     }
 
 
@@ -87,6 +94,60 @@ public class SQL2ParserTest {
         assertTrue(q.contains(token));
     }
 
+    /*
+     * When a query with LIMIT option, it should still select the index with 
the least entries
+     * as those might require being traversed during post-filtering.
+     *
+     * See OAK-12057
+     */
+    @Test
+    public void testPlanningWithLimit() throws ParseException {
+        // Given
+        var query = "SELECT * \n" +
+                "FROM [nt:base] AS s \n" +
+                "WHERE ISDESCENDANTNODE(s, '/content') AND 
s.[j:c]='/conf/wknd'\n" +
+                "OPTION (LIMIT 2)";
+
+        // - the first available option
+        var indexA = mock(DummyQueryIndex.class);
+        var planA = mock(QueryIndex.IndexPlan.class);
+        given(indexA.getPlans(any(), any(), any())).willReturn(List.of(planA));
+        given(planA.getPlanName()).willReturn("planA");
+        given(planA.getEstimatedEntryCount()).willReturn(10000L);   // more 
entries
+        given(planA.getCostPerEntry()).willReturn(1.0);
+        given(planA.getCostPerExecution()).willReturn(100.0);
+
+        // - the better option
+        var indexB = mock(DummyQueryIndex.class);
+        var planB = mock(QueryIndex.IndexPlan.class);
+        given(indexB.getPlans(any(), any(), any())).willReturn(List.of(planB));
+        given(planB.getPlanName()).willReturn("planB");
+        given(planB.getEstimatedEntryCount()).willReturn(100L);     // less 
entries
+        given(planB.getCostPerEntry()).willReturn(1.0);
+        given(planB.getCostPerExecution()).willReturn(100.0);
+        given(indexB.getPlanDescription(eq(planB), any())).willReturn("planB");
+
+        var indexProvider = new QueryIndexProvider() {
+            @Override
+            public @NotNull List<? extends QueryIndex> 
getQueryIndexes(NodeState nodeState) {
+                return List.of(indexA, indexB);
+            }
+        };
+
+        var context = mock(ExecutionContext.class);
+        given(context.getIndexProvider()).willReturn(indexProvider);
+
+        // When
+        var parsedQuery = p.parse(query,false);
+        parsedQuery.init();
+        parsedQuery.setExecutionContext(context);
+        parsedQuery.setTraversalEnabled(false);
+        parsedQuery.prepare();
+
+        // Then
+        assertEquals("[nt:base] as [s] /* planB */", parsedQuery.getPlan());
+    }
+
     @Test
     public void testCoalesce() throws ParseException {
         p.parse("SELECT * FROM [nt:base] WHERE COALESCE([j:c/m/d:t], 
[j:c/j:t])='a'");
@@ -188,4 +249,14 @@ public class SQL2ParserTest {
         xpath = "//(element(*, type1) | element(*, type2))[@a='b' or @c='d'] 
order by @foo";
         assertTrue("Converted xpath " + xpath + "doesn't end with 'order by 
[foo]'", c.convert(xpath).endsWith("order by [foo]"));
     }
+
+    interface DummyQueryIndex extends QueryIndex, 
QueryIndex.AdvancedQueryIndex {
+        /*
+         * This is needed as AdvancedQueryIndex is what we what to mock but 
the QueryIndexProvider
+         * returns QueryIndex instances and, in reality implementation of 
AdvancedQueryIndex also implement
+         * QueryIndex, but AdvancedQueryIndex does not explicitly extend 
QueryIndex.
+         *
+         * Making this link explicit would break the spi versioning.
+         */
+    }
 }
diff --git 
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/IndexCostEvaluationTest.java 
b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/IndexCostEvaluationTest.java
index f09e2eb21b..7f096cfc2a 100644
--- 
a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/IndexCostEvaluationTest.java
+++ 
b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/IndexCostEvaluationTest.java
@@ -17,8 +17,8 @@
 package org.apache.jackrabbit.oak;
 
 import ch.qos.logback.classic.Level;
-import org.apache.jackrabbit.api.JackrabbitRepository;
-import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.commons.junit.LogCustomizer;
 import org.apache.jackrabbit.oak.jcr.Jcr;
 import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexPlan;
@@ -28,6 +28,7 @@ import org.apache.jackrabbit.oak.spi.query.Cursor;
 import org.apache.jackrabbit.oak.spi.query.Filter;
 import org.apache.jackrabbit.oak.spi.query.QueryIndex;
 import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.jetbrains.annotations.NotNull;
 import org.junit.After;
@@ -36,66 +37,61 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
-import javax.jcr.Node;
-import javax.jcr.Repository;
-import javax.jcr.SimpleCredentials;
 import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 
+import static javax.jcr.query.Query.JCR_SQL2;
 import static org.junit.Assert.assertTrue;
 
 public class IndexCostEvaluationTest {
     @Rule
     public TemporaryFolder temporaryFolder = new TemporaryFolder(new 
File("target"));
 
-    private static final String TEST_USER_NAME = "testUserName";
-
-    private Repository repository = null;
-    private JackrabbitSession session = null;
-    private Node root = null;
-    private LogCustomizer custom;
+    private ContentSession session = null;
+    private LogCustomizer logCollector;
 
     @Before
     public void before() throws Exception {
-        custom = LogCustomizer
+        logCollector = LogCustomizer
                 .forLogger(
                         "org.apache.jackrabbit.oak.query.QueryImpl")
                 .enable(Level.DEBUG).create();
-        custom.starting();
-
-
-        TestIndexProvider testProvider = new TestIndexProvider();
-        TestIndexProvider2 testProvider2 = new TestIndexProvider2();
-        TestIndexProvider3 testProvider3 = new TestIndexProvider3();
-
-        Jcr jcr = new Jcr()
-                .with((QueryIndexProvider) testProvider)
-                .with((QueryIndexProvider) testProvider2)
-                .with((QueryIndexProvider) testProvider3);
-
-        repository = jcr.createRepository();
-        session = (JackrabbitSession) repository.login(new 
SimpleCredentials("admin", "admin".toCharArray()));
-        root = session.getRootNode();
+        logCollector.starting();
+        double luceneMinCost = 2.2;
+        double elasticMinCost = 2.1;
+        TestIndexProvider testProvider = new TestIndexProvider("test-index", 
luceneMinCost);
+        TestIndexProvider testProvider2 = new TestIndexProvider("test-index2", 
luceneMinCost);
+        TestIndexProvider testProvider3 = new TestIndexProvider("test-index3", 
elasticMinCost);
+
+        Jcr jcr = new Jcr(new Oak(), false)
+                .with(new OpenSecurityProvider())
+                .with(new InitialContent())
+                .with(testProvider)
+                .with(testProvider2)
+                .with(testProvider3);
+
+        ContentRepository repository = jcr.createContentRepository();
+        session = repository.login(null, null);
     }
 
-
     @After
-    public void after() {
-        custom.finished();
-        session.logout();
-        if (repository instanceof JackrabbitRepository) {
-            ((JackrabbitRepository) repository).shutdown();
-        }
+    public void after() throws IOException {
+        session.close();
+        logCollector.finished();
     }
 
     // In cases where two indexes have same min cost i.e. both indexes are on 
par, we don't skip cost evaluation
     // even of cost from previous index is less than min cost of new index.
     @Test
     public void costEvaluationTest() throws Exception {
+        String query = "SELECT * FROM [rep:Authorizable] WHERE 
[rep:principalName] = 'anonymous'";
+        session.getLatestRoot().getQueryEngine().executeQuery(query, JCR_SQL2, 
1, 0, null, null);
+
         boolean evaluationContinueLogPresent = false;
         boolean evaluationSkipLogPresent = false;
-        for (String log : custom.getLogs()) {
+        for (String log : logCollector.getLogs()) {
             if (log.equals("minCost: 2.1 of index :test-index2 > best Cost: 
2.0 from index: test-index, but both indexes have same minimum cost - cost 
evaluation will continue")) {
                 evaluationContinueLogPresent = true;
             }
@@ -107,8 +103,12 @@ public class IndexCostEvaluationTest {
         assertTrue(evaluationSkipLogPresent);
     }
 
-    private class TestIndexProvider implements QueryIndexProvider {
-        TestIndex index = new TestIndex();
+    private static class TestIndexProvider implements QueryIndexProvider {
+        private final TestIndex index;
+
+        public TestIndexProvider(String indexName, double minimumCost) {
+            this.index = new TestIndex(indexName, minimumCost);
+        }
 
         @Override
         public @NotNull List<? extends QueryIndex> getQueryIndexes(NodeState 
nodeState) {
@@ -116,35 +116,19 @@ public class IndexCostEvaluationTest {
         }
     }
 
-    private class TestIndexProvider2 extends TestIndexProvider {
-        public TestIndexProvider2() {
-            this.index = new TestIndex() {
-                public String getIndexName() {
-                    return "test-index2";
-                }
-            };
-        }
-    }
+    private static class TestIndex implements QueryIndex, 
QueryIndex.AdvancedQueryIndex {
 
-    private class TestIndexProvider3 extends TestIndexProvider {
-        public TestIndexProvider3() {
-            this.index = new TestIndex() {
-                public String getIndexName() {
-                    return "test-index3";
-                }
-
-                public double getMinimumCost() {
-                    return PropertyIndexPlan.COST_OVERHEAD + 0.11;
-                }
-            };
-        }
-    }
+        private final String name;
+        private final double minimumCost;
 
-    private class TestIndex implements QueryIndex, 
QueryIndex.AdvancedQueryIndex {
+        public TestIndex(String indexName, double minimumCost) {
+            this.name = indexName;
+            this.minimumCost = minimumCost;
+        }
 
         @Override
         public double getMinimumCost() {
-            return PropertyIndexPlan.COST_OVERHEAD + 0.1;
+            return minimumCost;
         }
 
         @Override
@@ -164,7 +148,7 @@ public class IndexCostEvaluationTest {
 
         @Override
         public String getIndexName() {
-            return "test-index";
+            return name;
         }
 
         @Override
@@ -172,7 +156,7 @@ public class IndexCostEvaluationTest {
             IndexPlan.Builder b = new IndexPlan.Builder();
             Filter f = new FilterImpl(null, "SELECT * FROM [nt:file]", new 
QueryEngineSettings());
             IndexPlan plan1 = 
b.setEstimatedEntryCount(10).setPlanName("testIndexPlan1").setFilter(f).build();
-            List<IndexPlan> indexList = new ArrayList<IndexPlan>();
+            List<IndexPlan> indexList = new ArrayList<>();
 
             indexList.add(plan1);
             return indexList;
diff --git 
a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java
 
b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java
index 0dcba9f79d..6bab6bb03e 100644
--- 
a/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java
+++ 
b/oak-query-spi/src/main/java/org/apache/jackrabbit/oak/spi/query/QueryIndex.java
@@ -34,7 +34,6 @@ import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static java.util.stream.Collectors.toMap;
 import static org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction;
 
 /**
@@ -373,9 +372,6 @@ public interface QueryIndex {
          * A builder for index plans.
          */
         class Builder {
-
-            private static Logger LOG = 
LoggerFactory.getLogger(QueryIndex.IndexPlan.Builder.class);
-
             protected double costPerExecution = 1.0;
             protected double costPerEntry = 1.0;
             protected long estimatedEntryCount = 1000000;

Reply via email to