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();
+ }
+ }
+}