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

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


The following commit(s) were added to refs/heads/OAK-12266 by this push:
     new 4570624bf3 OAK-12266: move fastQuerySize tests to oak-search for both 
backends (#2967)
4570624bf3 is described below

commit 4570624bf3ccbd8d5e2dc71f23f87319613eb3e4
Author: Theia Vlad <[email protected]>
AuthorDate: Tue Jun 30 14:08:39 2026 +0200

    OAK-12266: move fastQuerySize tests to oak-search for both backends (#2967)
    
    Relocate ResultSizeTest and WhiteboardResultSizeTest from oak-lucene into
    oak-search as JCR-level CommonTests with Lucene and Elasticsearch 
subclasses,
    so both backends exercise the fast query result size feature. The Lucene V1
    index-format case remains Lucene-only in LuceneResultSizeTest.
    
    Co-authored-by: Theia Vlad <[email protected]>
---
 .../jackrabbit/oak/jcr/query/ResultSizeTest.java   | 130 ----------
 .../oak/jcr/query/WhiteboardResultSizeTest.java    | 264 ---------------------
 .../plugins/index/lucene/LuceneResultSizeTest.java | 123 ++++++++++
 .../lucene/LuceneWhiteboardResultSizeTest.java     |  50 ++++
 .../index/elastic/ElasticResultSizeTest.java       |  47 ++++
 .../elastic/ElasticWhiteboardResultSizeTest.java   |  46 ++++
 .../oak/plugins/index/ResultSizeCommonTest.java    | 162 +++++++++++++
 .../index/WhiteboardResultSizeCommonTest.java      |  90 +++++++
 8 files changed, 518 insertions(+), 394 deletions(-)

diff --git 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/ResultSizeTest.java
 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/ResultSizeTest.java
deleted file mode 100644
index 5f5ad023eb..0000000000
--- 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/ResultSizeTest.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.jackrabbit.oak.jcr.query;
-
-import javax.jcr.Node;
-import javax.jcr.NodeIterator;
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-import javax.jcr.query.Query;
-import javax.jcr.query.QueryManager;
-
-import org.apache.jackrabbit.core.query.AbstractQueryTest;
-import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
-import org.apache.jackrabbit.oak.plugins.index.search.IndexFormatVersion;
-
-public class ResultSizeTest extends AbstractQueryTest {
-
-    public void testResultSize() throws Exception {
-        doTestResultSize(false);
-    }
-    
-    public void testResultSizeLuceneV1() throws Exception {
-        Session session = superuser;
-        Node index = session.getRootNode().getNode("oak:index");
-        Node luceneGlobal = index.getNode("luceneGlobal");
-        luceneGlobal.setProperty("type", "disabled");
-        Node luceneV1 = index.addNode("luceneV1", "oak:QueryIndexDefinition");
-        luceneV1.setProperty("type", "lucene");
-        luceneV1.setProperty(FulltextIndexConstants.COMPAT_MODE, 
IndexFormatVersion.V1.getVersion());
-        session.save();
-
-        try {
-            doTestResultSize(true);
-        } finally {
-            luceneV1.remove();
-            luceneGlobal.setProperty("type", "lucene");
-            luceneGlobal.setProperty("reindex", true);
-            session.save();
-        }
-    }
-    
-    private void doTestResultSize(boolean aggregateAtQueryTime) throws 
RepositoryException {
-        createData();
-        int expectedForUnion = 400;
-        int expectedForTwoConditions = aggregateAtQueryTime ? 400 : 200;
-        doTestResultSize(false, expectedForTwoConditions);
-        doTestResultSize(true, expectedForUnion);
-    }
-    
-    private void createData() throws RepositoryException {
-        Session session = superuser;
-        for (int i = 0; i < 200; i++) {
-            Node n = testRootNode.addNode("node" + i);
-            n.setProperty("text", "Hello World");
-        }
-        session.save();
-    }
-    
-    private void doTestResultSize(boolean union, int expected) throws 
RepositoryException {
-        Session session = superuser;
-        QueryManager qm = session.getWorkspace().getQueryManager();
-
-        String xpath;
-        if (union) {
-            xpath = "/jcr:root//*[jcr:contains(@text, 'Hello') or 
jcr:contains(@text, 'World')]";
-        } else {
-            xpath = "/jcr:root//*[jcr:contains(@text, 'Hello World')]";
-        }
-        
-        Query q;
-        long result;
-        NodeIterator it;
-        StringBuilder buff;
-        
-        // fast (insecure) case
-        // enabled by default now, in LuceneOakRepositoryStub 
-        System.clearProperty("oak.fastQuerySize");
-        q = qm.createQuery(xpath, "xpath");
-        it = q.execute().getNodes();
-        result = it.getSize();
-        assertTrue("size: " + result + " expected around " + expected, 
-                result > expected - 50 && 
-                result < expected + 50);
-        buff = new StringBuilder();
-        while (it.hasNext()) {
-            Node n = it.nextNode();
-            buff.append(n.getPath()).append('\n');
-        }
-        String fastSizeResult = buff.toString();
-        q = qm.createQuery(xpath, "xpath");
-        q.setLimit(90);
-        it = q.execute().getNodes();
-        assertEquals(90, it.getSize());
-        
-        
-        // default (secure) case
-        // manually disabled
-        System.setProperty("oak.fastQuerySize", "false");
-        q = qm.createQuery(xpath, "xpath");
-        it = q.execute().getNodes();
-        result = it.getSize();
-        assertEquals(-1, result);
-        buff = new StringBuilder();
-        while (it.hasNext()) {
-            Node n = it.nextNode();
-            buff.append(n.getPath()).append('\n');
-        }
-        String regularResult = buff.toString();
-        assertEquals(regularResult, fastSizeResult);
-        System.clearProperty("oak.fastQuerySize");
-        
-    }
-    
-}
\ No newline at end of file
diff --git 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/WhiteboardResultSizeTest.java
 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/WhiteboardResultSizeTest.java
deleted file mode 100644
index de5fbae473..0000000000
--- 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/WhiteboardResultSizeTest.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * 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.jackrabbit.oak.jcr.query;
-
-import org.apache.jackrabbit.api.JackrabbitRepository;
-import 
org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
-import org.apache.jackrabbit.oak.jcr.Jcr;
-import org.apache.jackrabbit.oak.jcr.LuceneOakRepositoryStub;
-import org.apache.jackrabbit.oak.spi.query.SessionQuerySettingsProvider;
-import org.apache.jackrabbit.oak.query.SessionQuerySettingsProviderService;
-import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
-import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
-import org.apache.jackrabbit.test.RepositoryStub;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import javax.jcr.Credentials;
-import javax.jcr.Node;
-import javax.jcr.NodeIterator;
-import javax.jcr.Repository;
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-import javax.jcr.query.Query;
-import javax.jcr.query.QueryManager;
-import javax.jcr.security.Privilege;
-import java.io.InputStream;
-import java.lang.reflect.Method;
-import java.util.Collections;
-import java.util.Properties;
-import java.util.concurrent.CompletableFuture;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Performs the same queries as {@link ResultSizeTest#testResultSize()} and 
expects the same results, with the only
- * differences being the registration of a {@link 
org.apache.jackrabbit.oak.spi.query.SessionQuerySettingsProvider} service
- * in the whiteboard, and the runtime reconfiguration of the service before 
each login, in place of the calls to
- * {@code System.setProperty("oak.fastQuerySize", true/false)}.
- */
-public class WhiteboardResultSizeTest {
-
-    /**
-     * Non-static inner class extending RepositoryStub which is intended to be 
created directly by a test
-     * and not by the TCK RepositoryHelper, purely so that it can mirror the 
behavior of
-     * {@link org.apache.jackrabbit.oak.jcr.query.ResultSizeTest} with the 
only differences being the registration of
-     * a {@link 
org.apache.jackrabbit.oak.spi.query.SessionQuerySettingsProvider} service in 
the whiteboard, and the
-     * runtime reconfiguration of the service before each login, in place of 
the calls to
-     * {@code System.setProperty("oak.fastQuerySize", true/false)}.
-     */
-    public class OakResultSizeStub extends LuceneOakRepositoryStub {
-        public OakResultSizeStub(Properties settings) throws 
RepositoryException {
-            super(settings);
-        }
-
-        @Override
-        protected void preCreateRepository(Jcr jcr, Whiteboard whiteboard) {
-            // register the SessionQuerySettingsProvider service before 
creation of the repository.
-            whiteboard.register(SessionQuerySettingsProvider.class,
-                    settingsProviderService,
-                    Collections.emptyMap());
-            super.preCreateRepository(jcr, whiteboard);
-        }
-    }
-
-    @SessionQuerySettingsProviderService.Configuration(directCountsPrincipals 
= {"admin"})
-    static class AdminAllowed {
-        /* class which hosts a config annotation for reflection */
-    }
-
-    @SessionQuerySettingsProviderService.Configuration
-    static class NoneAllowed {
-        /* class which hosts a config annotation for reflection */
-    }
-
-    private SessionQuerySettingsProviderService settingsProviderService =
-            new SessionQuerySettingsProviderService();
-    private RepositoryStub stub;
-    private volatile Repository repository;
-    private volatile Session adminSession;
-
-    /**
-     * Reconfigures the singleton {@link 
org.apache.jackrabbit.oak.query.SessionQuerySettingsProviderService} instance 
between queries.
-     *
-     * @param config a config annotation
-     * @throws Exception for reflection errors
-     */
-    private void reconfigure(SessionQuerySettingsProviderService.Configuration 
config) throws Exception {
-        Method method = 
SessionQuerySettingsProviderService.class.getDeclaredMethod("configure",
-                SessionQuerySettingsProviderService.Configuration.class);
-        method.setAccessible(true);
-        method.invoke(settingsProviderService, config);
-    }
-
-    protected Session getAdminSession() throws Exception {
-        if (adminSession == null) {
-            adminSession = createAdminSession();
-            AccessControlUtils.addAccessControlEntry(adminSession, "/", 
EveryonePrincipal.getInstance(),
-                    new String[]{Privilege.JCR_READ}, true);
-            adminSession.save();
-        }
-        return adminSession;
-    }
-
-    private void createData() throws Exception {
-        Session session = getAdminSession();
-        Node testRootNode = session.getRootNode().addNode("testroot", 
"nt:unstructured");
-        for (int i = 0; i < 200; i++) {
-            Node n = testRootNode.addNode("node" + i);
-            n.setProperty("text", "Hello World");
-        }
-        session.save();
-    }
-
-    @Test
-    public void testResultSize() throws Exception {
-        doTestResultSize(false);
-    }
-
-    private void doTestResultSize(boolean aggregateAtQueryTime) throws 
Exception {
-        createData();
-        int expectedForUnion = 400;
-        int expectedForTwoConditions = aggregateAtQueryTime ? 400 : 200;
-        doTestResultSize(false, expectedForTwoConditions);
-        doTestResultSize(true, expectedForUnion);
-    }
-
-    /**
-     * Compare with {@code 
org.apache.jackrabbit.oak.jcr.query.ResultSizeTest#doTestResultSize(boolean, 
int)}.
-     */
-    private void doTestResultSize(boolean union, int expected) throws 
Exception {
-        Session session = null;
-        QueryManager qm;
-        Query q;
-        long result;
-        NodeIterator it;
-        StringBuilder buff;
-
-        String xpath;
-        if (union) {
-            xpath = "/jcr:root//*[jcr:contains(@text, 'Hello') or 
jcr:contains(@text, 'World')]";
-        } else {
-            xpath = "/jcr:root//*[jcr:contains(@text, 'Hello World')]";
-        }
-
-        final CompletableFuture<String> fastSizeResult = new 
CompletableFuture<>();
-
-        
reconfigure(AdminAllowed.class.getAnnotation(SessionQuerySettingsProviderService.Configuration.class));
-        try {
-            session = this.createAdminSession();
-            assertEquals("nt:unstructured",
-                    
session.getNode("/testroot").getPrimaryNodeType().getName());
-            qm = session.getWorkspace().getQueryManager();
-            q = qm.createQuery(xpath, "xpath");
-            it = q.execute().getNodes();
-            result = it.getSize();
-            assertTrue("size: " + result + " expected around " + expected,
-                    result > expected - 50 &&
-                            result < expected + 50);
-            buff = new StringBuilder();
-            while (it.hasNext()) {
-                Node n = it.nextNode();
-                buff.append(n.getPath()).append('\n');
-            }
-            fastSizeResult.complete(buff.toString());
-            q = qm.createQuery(xpath, "xpath");
-            q.setLimit(90);
-            it = q.execute().getNodes();
-            assertEquals(90, it.getSize());
-        } finally {
-            if (session != null) {
-                session.logout();
-            }
-        }
-
-        
reconfigure(NoneAllowed.class.getAnnotation(SessionQuerySettingsProviderService.Configuration.class));
-        try {
-            session = this.createAdminSession();
-            qm = session.getWorkspace().getQueryManager();
-            q = qm.createQuery(xpath, "xpath");
-            it = q.execute().getNodes();
-            result = it.getSize();
-            assertEquals(-1, result);
-            buff = new StringBuilder();
-            while (it.hasNext()) {
-                Node n = it.nextNode();
-                buff.append(n.getPath()).append('\n');
-            }
-            String regularResult = buff.toString();
-            assertEquals(regularResult, fastSizeResult.get());
-        } finally {
-            if (session != null) {
-                session.logout();
-            }
-        }
-    }
-
-    // ----< repository initialization methods >--------
-
-    @Before
-    public void setUp() throws Exception {
-        getAdminSession();
-    }
-
-    @After
-    public void logout() {
-        if (adminSession != null) {
-            adminSession.logout();
-            adminSession = null;
-        }
-        // release repository field
-        if (repository instanceof JackrabbitRepository) {
-            ((JackrabbitRepository) repository).shutdown();
-        }
-        repository = null;
-        stub = null;
-    }
-
-    protected RepositoryStub getStub() throws Exception {
-        if (stub == null) {
-            Properties properties = new Properties();
-            try (InputStream is = 
RepositoryStub.class.getClassLoader().getResourceAsStream(RepositoryStub.STUB_IMPL_PROPS))
 {
-                if (is != null) {
-                    properties.load(is);
-                }
-            }
-            stub = new OakResultSizeStub(properties);
-        }
-        return stub;
-    }
-
-    protected Repository getRepository() throws Exception {
-        if (repository == null) {
-            repository = getStub().getRepository();
-        }
-        return repository;
-    }
-
-    protected Session createAdminSession() throws Exception {
-        return getRepository().login(getAdminCredentials());
-    }
-
-    protected Credentials getAdminCredentials() {
-        return stub.getSuperuserCredentials();
-    }
-
-}
diff --git 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneResultSizeTest.java
 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneResultSizeTest.java
new file mode 100644
index 0000000000..1850c42d87
--- /dev/null
+++ 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneResultSizeTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.jackrabbit.oak.plugins.index.lucene;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import org.apache.jackrabbit.oak.plugins.index.LuceneIndexOptions;
+import org.apache.jackrabbit.oak.plugins.index.ResultSizeCommonTest;
+import org.apache.jackrabbit.oak.plugins.index.aggregate.SimpleNodeAggregator;
+import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexFormatVersion;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.apache.jackrabbit.JcrConstants.NT_FILE;
+
+public class LuceneResultSizeTest extends ResultSizeCommonTest {
+
+    private final ExecutorService executorService = 
Executors.newFixedThreadPool(2);
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder(new 
File("target"));
+
+    @Override
+    protected Repository createJcrRepository() {
+        indexOptions = new LuceneIndexOptions();
+        repositoryOptionsUtil = new LuceneTestRepositoryBuilderWithAggregator(
+                executorService, temporaryFolder).build();
+        Oak oak = repositoryOptionsUtil.getOak();
+        return new Jcr(oak).withFastQueryResultSize(true).createRepository();
+    }
+
+    @After
+    public void shutdownExecutor() {
+        executorService.shutdown();
+    }
+
+    /**
+     * Lucene V1 index format uses the AggregateIndex query path when a 
NodeAggregator is
+     * registered. AggregateIndex evaluates "Hello World" as 
FullTextAnd(Hello, World) and
+     * creates a separate index sub-plan per term; 
IntersectionCursor.getSize() sums the two
+     * per-term sizes, so the conjunction query reports ~2*NODE_COUNT instead 
of ~NODE_COUNT.
+     * Lucene-format specific: no Elasticsearch equivalent.
+     */
+    @Test
+    public void testResultSizeLuceneV1() throws Exception {
+        Node oakIndex = adminSession.getRootNode().getNode("oak:index");
+        // disable the V2 index set up by createIndex() so only the V1 index 
is used
+        oakIndex.getNode(indexName).setProperty("type", "disabled");
+        // A bare V1 lucene index (no indexRules) triggers IndexDefinition's 
auto-rule
+        // creation path which produces an allProps rule with analyzed=true,
+        // making isFullTextEnabled()=true so LuceneIndex/AggregateIndex 
handle the query.
+        Node luceneV1 = oakIndex.addNode("luceneV1", 
"oak:QueryIndexDefinition");
+        luceneV1.setProperty("type", "lucene");
+        luceneV1.setProperty(FulltextIndexConstants.COMPAT_MODE, 
IndexFormatVersion.V1.getVersion());
+        luceneV1.setProperty("reindex", true);
+        adminSession.save();
+
+        try {
+            assertEventually(() -> {
+                try {
+                    // V1 aggregates term hits at query time: a conjunction 
"Hello World" is
+                    // parsed as FullTextAnd(Hello, World), each term is 
estimated independently
+                    // and sizes summed by IntersectionCursor → ~2*NODE_COUNT
+                    doTestResultSize(false, 2 * NODE_COUNT);
+                    doTestResultSize(true, 2 * NODE_COUNT);
+                } catch (Exception e) {
+                    throw new RuntimeException(e);
+                }
+            });
+        } finally {
+            luceneV1.remove();
+            oakIndex.getNode(indexName).setProperty("type", "lucene");
+            oakIndex.getNode(indexName).setProperty("reindex", true);
+            adminSession.save();
+        }
+    }
+
+    /**
+     * Extends LuceneTestRepositoryBuilder to register a NodeAggregator on the
+     * LuceneIndexProvider. The aggregator's specific rule 
(nt:file/jcr:content) is
+     * irrelevant to the test data; its mere presence enables AggregateIndex's 
composite-plan
+     * (per-term) path, which is required for testResultSizeLuceneV1 to report 
~2*NODE_COUNT.
+     * It does not affect testResultSize because the V2 index (used by that 
test) is not
+     * handled by LuceneIndex, so AggregateIndex produces no plan for V2 
queries.
+     */
+    private static class LuceneTestRepositoryBuilderWithAggregator extends 
LuceneTestRepositoryBuilder {
+        LuceneTestRepositoryBuilderWithAggregator(ExecutorService 
executorService,
+                TemporaryFolder temporaryFolder) {
+            super(executorService, temporaryFolder);
+            // indexProvider is the LuceneIndexProvider (protected field from 
TestRepositoryBuilder).
+            // Setting the aggregator here, after super() creates 
resultCountingIndexProvider,
+            // still works because ResultCountingIndexProvider delegates 
getQueryIndexes() to
+            // indexProvider, which reads this.aggregator lazily when building 
AggregateIndex.
+            ((LuceneIndexProvider) indexProvider).with(
+                    new SimpleNodeAggregator().newRuleWithName(
+                            NT_FILE, List.of("jcr:content")));
+        }
+    }
+}
diff --git 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneWhiteboardResultSizeTest.java
 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneWhiteboardResultSizeTest.java
new file mode 100644
index 0000000000..658c9b71da
--- /dev/null
+++ 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneWhiteboardResultSizeTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.jackrabbit.oak.plugins.index.lucene;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.plugins.index.LuceneIndexOptions;
+import org.apache.jackrabbit.oak.plugins.index.WhiteboardResultSizeCommonTest;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+import javax.jcr.Repository;
+import java.io.File;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class LuceneWhiteboardResultSizeTest extends 
WhiteboardResultSizeCommonTest {
+
+    private final ExecutorService executorService = 
Executors.newFixedThreadPool(2);
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder(new 
File("target"));
+
+    @Override
+    protected Repository createJcrRepository() {
+        indexOptions = new LuceneIndexOptions();
+        repositoryOptionsUtil = new 
LuceneTestRepositoryBuilder(executorService, temporaryFolder).build();
+        Oak oak = repositoryOptionsUtil.getOak();
+        return buildRepository(oak);
+    }
+
+    @After
+    public void shutdownExecutor() {
+        executorService.shutdown();
+    }
+}
diff --git 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticResultSizeTest.java
 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticResultSizeTest.java
new file mode 100644
index 0000000000..b8439891f1
--- /dev/null
+++ 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticResultSizeTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.jackrabbit.oak.plugins.index.elastic;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import org.apache.jackrabbit.oak.plugins.index.ResultSizeCommonTest;
+import org.apache.jackrabbit.oak.plugins.index.TestUtil;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.index.ElasticBulkProcessorHandler;
+import org.junit.ClassRule;
+
+import javax.jcr.Repository;
+
+public class ElasticResultSizeTest extends ResultSizeCommonTest {
+
+    @ClassRule
+    public static final ElasticConnectionRule elasticRule = new 
ElasticConnectionRule();
+
+    @Override
+    protected Repository createJcrRepository() {
+        indexOptions = new ElasticIndexOptions();
+        repositoryOptionsUtil = new 
ElasticTestRepositoryBuilder(elasticRule).build();
+        Oak oak = repositoryOptionsUtil.getOak();
+        return new Jcr(oak).withFastQueryResultSize(true).createRepository();
+    }
+
+    @Override
+    protected void assertEventually(Runnable r) {
+        TestUtil.assertEventually(r,
+                ((repositoryOptionsUtil.isAsync() ? 
repositoryOptionsUtil.defaultAsyncIndexingTimeInSeconds : 2000)
+                        + 
ElasticBulkProcessorHandler.BULK_FLUSH_INTERVAL_MS_DEFAULT) * 5);
+    }
+}
diff --git 
a/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticWhiteboardResultSizeTest.java
 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticWhiteboardResultSizeTest.java
new file mode 100644
index 0000000000..e9510d3aa4
--- /dev/null
+++ 
b/oak-search-elastic/src/test/java/org/apache/jackrabbit/oak/plugins/index/elastic/ElasticWhiteboardResultSizeTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.jackrabbit.oak.plugins.index.elastic;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.plugins.index.TestUtil;
+import org.apache.jackrabbit.oak.plugins.index.WhiteboardResultSizeCommonTest;
+import 
org.apache.jackrabbit.oak.plugins.index.elastic.index.ElasticBulkProcessorHandler;
+import org.junit.ClassRule;
+
+import javax.jcr.Repository;
+
+public class ElasticWhiteboardResultSizeTest extends 
WhiteboardResultSizeCommonTest {
+
+    @ClassRule
+    public static final ElasticConnectionRule elasticRule = new 
ElasticConnectionRule();
+
+    @Override
+    protected Repository createJcrRepository() {
+        indexOptions = new ElasticIndexOptions();
+        repositoryOptionsUtil = new 
ElasticTestRepositoryBuilder(elasticRule).build();
+        Oak oak = repositoryOptionsUtil.getOak();
+        return buildRepository(oak);
+    }
+
+    @Override
+    protected void assertEventually(Runnable r) {
+        TestUtil.assertEventually(r,
+                ((repositoryOptionsUtil.isAsync() ? 
repositoryOptionsUtil.defaultAsyncIndexingTimeInSeconds : 2000)
+                        + 
ElasticBulkProcessorHandler.BULK_FLUSH_INTERVAL_MS_DEFAULT) * 5);
+    }
+}
diff --git 
a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/ResultSizeCommonTest.java
 
b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/ResultSizeCommonTest.java
new file mode 100644
index 0000000000..8df9f6d189
--- /dev/null
+++ 
b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/ResultSizeCommonTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.jackrabbit.oak.plugins.index;
+
+import 
org.apache.jackrabbit.oak.plugins.index.search.util.IndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.query.AbstractJcrTest;
+import org.junit.After;
+import org.junit.Test;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verifies the fast (insecure) query result size feature across index 
backends.
+ * Ported from the Lucene-only {@code 
org.apache.jackrabbit.oak.jcr.query.ResultSizeTest}.
+ * This base drives the feature through the {@code oak.fastQuerySize} system 
property;
+ * {@code WhiteboardResultSizeCommonTest} overrides the toggle hooks to drive 
it through
+ * a registered {@code SessionQuerySettingsProvider}.
+ */
+public abstract class ResultSizeCommonTest extends AbstractJcrTest {
+
+    protected IndexOptions indexOptions;
+    protected TestRepository repositoryOptionsUtil;
+    protected String indexName;
+
+    protected static final int NODE_COUNT = 200;
+    private static final int TOLERANCE = 50;
+
+    protected void assertEventually(Runnable r) {
+        TestUtil.assertEventually(r,
+                ((repositoryOptionsUtil.isAsync() ? 
repositoryOptionsUtil.defaultAsyncIndexingTimeInSeconds : 0) + 3000) * 5);
+    }
+
+    @Override
+    protected void initialize() {
+        try {
+            createIndex();
+            createData();
+        } catch (RepositoryException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void createIndex() throws RepositoryException {
+        IndexDefinitionBuilder builder = 
indexOptions.createIndexDefinitionBuilder();
+        
builder.indexRule("nt:base").property("text").analyzed().nodeScopeIndex();
+        indexName = "resultsize_" + UUID.randomUUID();
+        indexOptions.setIndex(adminSession, indexName, 
indexOptions.createIndex(builder, false, "text"));
+    }
+
+    private void createData() throws RepositoryException {
+        Node testRoot = adminSession.getRootNode().addNode("testroot", 
"nt:unstructured");
+        for (int i = 0; i < NODE_COUNT; i++) {
+            testRoot.addNode("node" + i).setProperty("text", "Hello World");
+        }
+        adminSession.save();
+    }
+
+    /** Enable (fast) / disable (secure) direct result counts for subsequent 
queries. */
+    protected void setDirectResultCount(boolean fast) {
+        if (fast) {
+            System.clearProperty("oak.fastQuerySize");
+        } else {
+            System.setProperty("oak.fastQuerySize", "false");
+        }
+    }
+
+    /** Session used to run the size queries. Plain variant reuses the admin 
session. */
+    protected Session querySession() throws RepositoryException {
+        return adminSession;
+    }
+
+    /** Release a session obtained from {@link #querySession()} (no-op for the 
reused admin session). */
+    protected void releaseQuerySession(Session session) {
+    }
+
+    @After
+    public void clearFastQuerySizeProperty() {
+        System.clearProperty("oak.fastQuerySize");
+    }
+
+    @Test
+    public void testResultSize() {
+        assertEventually(() -> {
+            try {
+                doTestResultSize(false, NODE_COUNT);      // conjunction
+                doTestResultSize(true, 2 * NODE_COUNT);   // union (XPath 'or' 
sums subquery sizes)
+            } catch (RepositoryException e) {
+                throw new RuntimeException(e);
+            }
+        });
+    }
+
+    protected void doTestResultSize(boolean union, int expected) throws 
RepositoryException {
+        String xpath = union
+                ? "/jcr:root/testroot//*[jcr:contains(@text, 'Hello') or 
jcr:contains(@text, 'World')]"
+                : "/jcr:root/testroot//*[jcr:contains(@text, 'Hello World')]";
+
+        // fast (insecure) case
+        setDirectResultCount(true);
+        Session fastSession = querySession();
+        String fastPaths;
+        try {
+            QueryManager qm = fastSession.getWorkspace().getQueryManager();
+            Query q = qm.createQuery(xpath, "xpath");
+            NodeIterator it = q.execute().getNodes();
+            long size = it.getSize();
+            assertTrue("size: " + size + " expected around " + expected,
+                    size > expected - TOLERANCE && size < expected + 
TOLERANCE);
+            StringBuilder buff = new StringBuilder();
+            while (it.hasNext()) {
+                buff.append(it.nextNode().getPath()).append('\n');
+            }
+            fastPaths = buff.toString();
+
+            q = qm.createQuery(xpath, "xpath");
+            q.setLimit(90);
+            assertEquals(90, q.execute().getNodes().getSize());
+        } finally {
+            releaseQuerySession(fastSession);
+        }
+
+        // default (secure) case
+        setDirectResultCount(false);
+        Session secureSession = querySession();
+        try {
+            QueryManager qm = secureSession.getWorkspace().getQueryManager();
+            Query q = qm.createQuery(xpath, "xpath");
+            NodeIterator it = q.execute().getNodes();
+            assertEquals(-1, it.getSize());
+            StringBuilder buff = new StringBuilder();
+            while (it.hasNext()) {
+                buff.append(it.nextNode().getPath()).append('\n');
+            }
+            assertEquals(buff.toString(), fastPaths);
+        } finally {
+            releaseQuerySession(secureSession);
+        }
+    }
+}
diff --git 
a/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/WhiteboardResultSizeCommonTest.java
 
b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/WhiteboardResultSizeCommonTest.java
new file mode 100644
index 0000000000..e365ad7985
--- /dev/null
+++ 
b/oak-search/src/test/java/org/apache/jackrabbit/oak/plugins/index/WhiteboardResultSizeCommonTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.jackrabbit.oak.plugins.index;
+
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.jcr.Jcr;
+import org.apache.jackrabbit.oak.query.SessionQuerySettingsProviderService;
+import org.apache.jackrabbit.oak.spi.query.SessionQuerySettingsProvider;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import java.lang.reflect.Method;
+import java.util.Collections;
+
+/**
+ * Same result-size assertions as {@link ResultSizeCommonTest}, but the fast 
(insecure)
+ * count is gated by a {@code SessionQuerySettingsProvider} registered on the 
Whiteboard
+ * and reconfigured per principal, instead of the
+ * {@code oak.fastQuerySize} system property. Ported from the Lucene-only
+ * {@code org.apache.jackrabbit.oak.jcr.query.WhiteboardResultSizeTest}.
+ */
+public abstract class WhiteboardResultSizeCommonTest extends 
ResultSizeCommonTest {
+
+    @SessionQuerySettingsProviderService.Configuration(directCountsPrincipals 
= {"admin"})
+    static class AdminAllowed {
+    }
+
+    @SessionQuerySettingsProviderService.Configuration
+    static class NoneAllowed {
+    }
+
+    protected final SessionQuerySettingsProviderService 
settingsProviderService =
+            new SessionQuerySettingsProviderService();
+    private Repository repository;
+
+    /** Register the settings provider on the whiteboard before creating the 
repository. */
+    protected Repository buildRepository(Oak oak) {
+        Whiteboard whiteboard = oak.getWhiteboard();
+        whiteboard.register(SessionQuerySettingsProvider.class, 
settingsProviderService, Collections.emptyMap());
+        repository = new Jcr(oak).createRepository();
+        return repository;
+    }
+
+    private void reconfigure(Class<?> configHolder) {
+        try {
+            Method m = SessionQuerySettingsProviderService.class
+                    .getDeclaredMethod("configure", 
SessionQuerySettingsProviderService.Configuration.class);
+            m.setAccessible(true);
+            m.invoke(settingsProviderService,
+                    
configHolder.getAnnotation(SessionQuerySettingsProviderService.Configuration.class));
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    protected void setDirectResultCount(boolean fast) {
+        reconfigure(fast ? AdminAllowed.class : NoneAllowed.class);
+    }
+
+    @Override
+    protected Session querySession() throws RepositoryException {
+        // provider settings are captured at login -> re-login to pick up the 
reconfiguration
+        return repository.login(new SimpleCredentials("admin", 
"admin".toCharArray()), null);
+    }
+
+    @Override
+    protected void releaseQuerySession(Session session) {
+        if (session != null) {
+            session.logout();
+        }
+    }
+}


Reply via email to