This is an automated email from the ASF dual-hosted git repository.
thomasm pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new dfd374c18f OAK-12007 XPath with or conditions should not always be
converted to union (#2613)
dfd374c18f is described below
commit dfd374c18fbf8d9979ed8f44ad15489b12d36717
Author: Thomas Mueller <[email protected]>
AuthorDate: Fri Nov 28 09:00:01 2025 +0100
OAK-12007 XPath with or conditions should not always be converted to union
(#2613)
* OAK-12007 XPath with or conditions should not always be converted to union
* OAK-12007 XPath with or conditions should not always be converted to union
* OAK-12007 XPath with or conditions should not always be converted to union
* OAK-12007 XPath with or conditions should not always be converted to union
---
.../main/java/org/apache/jackrabbit/oak/Oak.java | 8 +
.../jackrabbit/oak/query/QueryEngineSettings.java | 12 ++
.../oak/query/ast/AstElementFactory.java | 23 ++-
.../oak/query/ast/FullTextSearchImpl.java | 30 +---
.../oak/query/ast/NotFullTextSearchImpl.java | 8 +-
.../jackrabbit/oak/query/xpath/Statement.java | 13 +-
.../oak/query/xpath/XPathToSQL2Converter.java | 3 +-
.../oak/query/QueryCostOverheadTest.java | 38 ++---
.../oak/query/xpath/XPathToSQL2Test.java | 171 +++++++++++++++++++++
9 files changed, 249 insertions(+), 57 deletions(-)
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
index 3f39239168..67431ee879 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java
@@ -593,6 +593,10 @@ public class Oak {
LOG.info("Registered sort union query by score feature: " +
QueryEngineSettings.FT_SORT_UNION_QUERY_BY_SCORE);
closer.register(sortUnionQueryByScoreFeature);
queryEngineSettings.setSortUnionQueryByScoreFeature(sortUnionQueryByScoreFeature);
+ Feature optimizeXPathUnion =
newFeature(QueryEngineSettings.FT_OPTIMIZE_XPATH_UNION, whiteboard);
+ LOG.info("Registered optimize XPath union feature: " +
QueryEngineSettings.FT_OPTIMIZE_XPATH_UNION);
+ closer.register(optimizeXPathUnion);
+ queryEngineSettings.setOptimizeXPathUnion(optimizeXPathUnion);
}
return this;
@@ -1008,6 +1012,10 @@ public class Oak {
settings.setSortUnionQueryByScoreFeature(feature);
}
+ public void setOptimizeXPathUnion(@Nullable Feature feature) {
+ settings.setOptimizeXPathUnion(feature);
+ }
+
@Override
public void setQueryValidatorPattern(String key, String pattern,
String comment, boolean failQuery) {
settings.getQueryValidator().setPattern(key, pattern, comment,
failQuery);
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
index 9a143f91ec..f34edce0b0 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryEngineSettings.java
@@ -69,6 +69,8 @@ public class QueryEngineSettings implements
QueryEngineSettingsMBean, QueryLimit
public static final String FT_SORT_UNION_QUERY_BY_SCORE = "FT_OAK-11949";
+ public static final String FT_OPTIMIZE_XPATH_UNION = "FT_OAK-12007";
+
public static final int DEFAULT_PREFETCH_COUNT =
Integer.getInteger(OAK_QUERY_PREFETCH_COUNT, -1);
public static final String OAK_QUERY_FAIL_TRAVERSAL =
"oak.queryFailTraversal";
@@ -127,6 +129,7 @@ public class QueryEngineSettings implements
QueryEngineSettingsMBean, QueryLimit
private Feature improvedIsNullCostFeature;
private Feature optimizeInRestrictionsForFunctions;
private Feature sortUnionQueryByScoreFeature;
+ private Feature optimizeXPathUnion;
private String autoOptionsMappingJson = "{}";
private QueryOptions.AutomaticQueryOptionsMapping autoOptionsMapping = new
QueryOptions.AutomaticQueryOptionsMapping(autoOptionsMappingJson);
@@ -260,6 +263,15 @@ public class QueryEngineSettings implements
QueryEngineSettingsMBean, QueryLimit
return sortUnionQueryByScoreFeature != null &&
sortUnionQueryByScoreFeature.isEnabled();
}
+ public void setOptimizeXPathUnion(@Nullable Feature feature) {
+ this.optimizeXPathUnion = feature;
+ }
+
+ public boolean isOptimizeXPathUnionEnabled() {
+ // disable if the feature toggle is not used
+ return optimizeXPathUnion != null && optimizeXPathUnion.isEnabled();
+ }
+
public String getStrictPathRestriction() {
return strictPathRestriction.name();
}
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
index 91510974f1..c0e56b0b5c 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/AstElementFactory.java
@@ -86,7 +86,20 @@ public class AstElementFactory {
public FullTextSearchImpl fullTextSearch(String selectorName, String
propertyName,
StaticOperandImpl fullTextSearchExpression) {
- return new FullTextSearchImpl(selectorName, propertyName,
fullTextSearchExpression);
+
+ int slash = -1;
+ if (propertyName != null) {
+ slash = propertyName.lastIndexOf('/');
+ }
+ String relativePath = null;
+ if (slash >= 0) {
+ relativePath = propertyName.substring(0, slash);
+ propertyName = propertyName.substring(slash + 1);
+ }
+ if ("*".equals(propertyName)) {
+ propertyName = null;
+ }
+ return new FullTextSearchImpl(selectorName, relativePath,
propertyName, fullTextSearchExpression);
}
public FullTextSearchScoreImpl fullTextSearchScore(String selectorName) {
@@ -136,7 +149,7 @@ public class AstElementFactory {
public PropertyExistenceImpl propertyExistence(String selectorName, String
propertyName) {
return new PropertyExistenceImpl(selectorName, propertyName);
}
-
+
public PropertyInexistenceImpl propertyInexistence(String selectorName,
String propertyName) {
return new PropertyInexistenceImpl(selectorName, propertyName);
}
@@ -186,7 +199,7 @@ public class AstElementFactory {
public ConstraintImpl suggest(String selectorName, StaticOperandImpl
expression) {
return new SuggestImpl(selectorName, expression);
}
-
+
/**
* <p>
* as the {@link AstElement#copyOf()} can return {@code this} is the
cloning is not implemented
@@ -199,13 +212,13 @@ public class AstElementFactory {
@NotNull
public static AstElement copyElementAndCheckReference(@NotNull final
AstElement e) {
AstElement clone = requireNonNull(e).copyOf();
-
+
if (clone == e && LOG.isDebugEnabled()) {
LOG.debug(
"Failed to clone the AstElement. Returning same reference; the
client may fail. {} - {}",
e.getClass().getName(), e);
}
-
+
return clone;
}
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
index 2b800f7d9a..11a8429d8f 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/FullTextSearchImpl.java
@@ -44,33 +44,17 @@ import static org.apache.jackrabbit.oak.api.Type.STRINGS;
public class FullTextSearchImpl extends ConstraintImpl {
final String selectorName;
- private final String relativePath;
+ final String relativePath;
final String propertyName;
final StaticOperandImpl fullTextSearchExpression;
private SelectorImpl selector;
public FullTextSearchImpl(
- String selectorName, String propertyName,
+ String selectorName, String relativePath, String propertyName,
StaticOperandImpl fullTextSearchExpression) {
this.selectorName = selectorName;
-
- int slash = -1;
- if (propertyName != null) {
- slash = propertyName.lastIndexOf('/');
- }
- if (slash == -1) {
- this.relativePath = null;
- } else {
- this.relativePath = propertyName.substring(0, slash);
- propertyName = propertyName.substring(slash + 1);
- }
-
- if (propertyName == null || "*".equals(propertyName)) {
- this.propertyName = null;
- } else {
- this.propertyName = propertyName;
- }
-
+ this.relativePath = relativePath;
+ this.propertyName = propertyName;
this.fullTextSearchExpression = fullTextSearchExpression;
}
@@ -253,7 +237,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
private static void appendString(StringBuilder buff, PropertyValue p) {
if (p.isArray()) {
if (p.getType() == Type.BINARIES) {
- // OAK-2050: don't try to load binaries as this would
+ // OAK-2050: don't try to load binaries as this would
// run out of memory
} else {
for (String v : p.getValue(STRINGS)) {
@@ -262,7 +246,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
}
} else {
if (p.getType() == Type.BINARY) {
- // OAK-2050: don't try to load binaries as this would
+ // OAK-2050: don't try to load binaries as this would
// run out of memory
} else {
buff.append(p.getValue(STRING)).append(' ');
@@ -299,7 +283,7 @@ public class FullTextSearchImpl extends ConstraintImpl {
@Override
public AstElement copyOf() {
- return new FullTextSearchImpl(selectorName, propertyName,
fullTextSearchExpression);
+ return new FullTextSearchImpl(selectorName, relativePath,
propertyName, fullTextSearchExpression);
}
@Override
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotFullTextSearchImpl.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotFullTextSearchImpl.java
index 79fdda47ee..1559662d22 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotFullTextSearchImpl.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/NotFullTextSearchImpl.java
@@ -29,18 +29,18 @@ import org.jetbrains.annotations.NotNull;
public class NotFullTextSearchImpl extends FullTextSearchImpl {
private static final Set<String> KEYWORDS = Set.of("or");
- public NotFullTextSearchImpl(String selectorName, String propertyName,
+ public NotFullTextSearchImpl(String selectorName, String relativePath,
String propertyName,
StaticOperandImpl fullTextSearchExpression) {
- super(selectorName, propertyName, fullTextSearchExpression);
+ super(selectorName, relativePath, propertyName,
fullTextSearchExpression);
}
public NotFullTextSearchImpl(FullTextSearchImpl ft) {
- this(ft.selectorName, ft.propertyName, ft.fullTextSearchExpression);
+ this(ft.selectorName, ft.relativePath, ft.propertyName,
ft.fullTextSearchExpression);
}
@Override
ConstraintImpl not() {
- return new FullTextSearchImpl(this.selectorName, this.propertyName,
+ return new FullTextSearchImpl(this.selectorName, this.relativePath,
this.propertyName,
this.fullTextSearchExpression);
}
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Statement.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Statement.java
index ad333c0ecc..8a97a120f7 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Statement.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/Statement.java
@@ -62,13 +62,16 @@ public class Statement {
QueryOptions queryOptions;
- public Statement optimize() {
+ public Statement optimize(boolean convertOrToUnion) {
ignoreOrderByScoreDesc(orderList);
if (where == null) {
return this;
}
where = where.optimize();
optimizeSelectorNodeTypes();
+ if (!convertOrToUnion) {
+ return this;
+ }
ArrayList<Expression> unionList = new ArrayList<Expression>();
try {
addToUnionList(where, unionList);
@@ -90,7 +93,7 @@ public class Statement {
if (union == null) {
union = s;
} else {
- union = new UnionStatement(union.optimize(), s.optimize());
+ union = new UnionStatement(union.optimize(convertOrToUnion),
s.optimize(convertOrToUnion));
}
}
union.orderList = orderList;
@@ -303,12 +306,12 @@ public class Statement {
}
@Override
- public Statement optimize() {
+ public Statement optimize(boolean convertOrToUnion) {
if (!KEEP_UNION_ORDER) {
ignoreOrderByScoreDesc(orderList);
}
- Statement s1b = s1.optimize();
- Statement s2b = s2.optimize();
+ Statement s1b = s1.optimize(convertOrToUnion);
+ Statement s2b = s2.optimize(convertOrToUnion);
if (s1 == s1b && s2 == s2b) {
// no change
return this;
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
index 74ab7e3569..514d477262 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Converter.java
@@ -97,7 +97,8 @@ public class XPathToSQL2Converter {
*/
public String convert(String query) throws ParseException {
Statement statement = convertToStatement(query);
- statement = statement.optimize();
+ boolean convertOrToUnion = settings == null ||
!settings.isOptimizeXPathUnionEnabled();
+ statement = statement.optimize(convertOrToUnion);
return statement.toString();
}
diff --git
a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/QueryCostOverheadTest.java
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/QueryCostOverheadTest.java
index 77298990ed..f63f2e9ab2 100644
---
a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/QueryCostOverheadTest.java
+++
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/QueryCostOverheadTest.java
@@ -36,52 +36,52 @@ public class QueryCostOverheadTest {
QueryImpl query;
UnionQueryImpl union;
ConstraintImpl c, c1, c2, c3, c4, c5;
-
+
c1 = new ComparisonImpl(null, null, null);
- c2 = new FullTextSearchImpl(null, null, null);
+ c2 = new FullTextSearchImpl(null, null, null, null);
union = new UnionQueryImpl(false,
createQuery(c1),
createQuery(c2),
null);
- assertFalse("we always expect false from a `UnionQueryImpl`",
+ assertFalse("we always expect false from a `UnionQueryImpl`",
union.containsUnfilteredFullTextCondition());
-
+
c1 = new ComparisonImpl(null, null, null);
- c2 = new FullTextSearchImpl(null, null, null);
+ c2 = new FullTextSearchImpl(null, null, null, null);
c = new OrImpl(c1, c2);
query = createQuery(c);
assertTrue(query.containsUnfilteredFullTextCondition());
c1 = new ComparisonImpl(null, null, null);
- c2 = new FullTextSearchImpl(null, null, null);
- c3 = new FullTextSearchImpl(null, null, null);
+ c2 = new FullTextSearchImpl(null, null, null, null);
+ c3 = new FullTextSearchImpl(null, null, null, null);
c = new OrImpl(List.of(c1, c2, c3));
query = createQuery(c);
assertTrue(query.containsUnfilteredFullTextCondition());
-
- c2 = new FullTextSearchImpl(null, null, null);
- c3 = new FullTextSearchImpl(null, null, null);
+
+ c2 = new FullTextSearchImpl(null, null, null, null);
+ c3 = new FullTextSearchImpl(null, null, null, null);
c4 = new ComparisonImpl(null, null, null);
c1 = new OrImpl(List.of(c2, c3, c4));
c5 = mock(DescendantNodeImpl.class);
c = new AndImpl(c1, c5);
query = createQuery(c);
assertTrue(query.containsUnfilteredFullTextCondition());
-
- c = new FullTextSearchImpl(null, null, null);
+
+ c = new FullTextSearchImpl(null, null, null, null);
query = createQuery(c);
assertFalse(query.containsUnfilteredFullTextCondition());
- c1 = new FullTextSearchImpl(null, null, null);
- c2 = new FullTextSearchImpl(null, null, null);
- c3 = new FullTextSearchImpl(null, null, null);
+ c1 = new FullTextSearchImpl(null, null, null, null);
+ c2 = new FullTextSearchImpl(null, null, null, null);
+ c3 = new FullTextSearchImpl(null, null, null, null);
c = new OrImpl(List.of(c1, c2, c3));
query = createQuery(c);
assertFalse(query.containsUnfilteredFullTextCondition());
-
+
c1 = new ComparisonImpl(null, null, null);
- c2 = new FullTextSearchImpl(null, null, null);
- c3 = new FullTextSearchImpl(null, null, null);
+ c2 = new FullTextSearchImpl(null, null, null, null);
+ c3 = new FullTextSearchImpl(null, null, null, null);
c = new AndImpl(List.of(c1, c2, c3));
query = createQuery(c);
assertFalse(query.containsUnfilteredFullTextCondition());
@@ -92,7 +92,7 @@ public class QueryCostOverheadTest {
query = createQuery(c);
assertFalse(query.containsUnfilteredFullTextCondition());
}
-
+
QueryImpl createQuery(ConstraintImpl c) {
return new QueryImpl(null, null, c, null, null, null, null);
}
diff --git
a/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java
new file mode 100644
index 0000000000..33334e954e
--- /dev/null
+++
b/oak-core/src/test/java/org/apache/jackrabbit/oak/query/xpath/XPathToSQL2Test.java
@@ -0,0 +1,171 @@
+/*
+ * 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.query.xpath;
+
+import static org.junit.Assert.assertEquals;
+
+import java.text.ParseException;
+
+import org.apache.jackrabbit.oak.query.QueryEngineSettings;
+import org.apache.jackrabbit.oak.spi.toggle.Feature;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * Tests for XPathToSQL2Converter focusing on the OR-to-UNION conversion
behavior
+ * controlled by the optimizeXPathUnion feature toggle.
+ */
+public class XPathToSQL2Test {
+
+ @Test
+ public void testOrToUnionConversionFeatureDisabled() throws ParseException
{
+ // Default behavior (feature disabled): OR conditions should be
converted to UNION
+ verify("//*[@a = 1 or @b = 2]",
+ false,
+ "select ... where [a] = 1 union select ... where [b] = 2
");
+ }
+
+ @Test
+ public void testOrToUnionConversionFeatureEnabled() throws ParseException {
+ // When feature is enabled: OR conditions should NOT be converted to
UNION
+ verify("//*[@a = 1 or @b = 2]",
+ true,
+ "select ... where [a] = 1 or [b] = 2 ");
+ }
+
+ @Test
+ public void testMultipleOrConditionsFeatureDisabled() throws
ParseException {
+ // Test multiple OR conditions with feature disabled (default)
+ verify("//*[@a = 1 or @b = 2 or @c = 3]",
+ false,
+ "select ... where [a] = 1 union select ... where [b] = 2 union
select ... where [c] = 3 ");
+ }
+
+ @Test
+ public void testMultipleOrConditionsFeatureEnabled() throws ParseException
{
+ // Test multiple OR conditions with feature enabled
+ verify("//*[@a = 1 or @b = 2 or @c = 3]",
+ true,
+ "select ... where [a] = 1 or [b] = 2 or [c] = 3 ");
+ }
+
+ @Test
+ public void testNestedOrWithAndFeatureDisabled() throws ParseException {
+ // Test nested OR with AND conditions, feature disabled
+ verify("//*[@x = 1 and (@a = 1 or @b = 2)]",
+ false,
+ "select ... where [x] = 1 and [a] = 1 union select ... where
[x] = 1 and [b] = 2 ");
+ }
+
+ @Test
+ public void testNestedOrWithAndFeatureEnabled() throws ParseException {
+ // Test nested OR with AND conditions, feature enabled
+ verify("//*[@x = 1 and (@a = 1 or @b = 2)]",
+ true,
+ "select ... where [x] = 1 and ([a] = 1 or [b] = 2) ");
+ }
+
+ @Test
+ public void testSimpleOrQueryFeatureDisabled() throws ParseException {
+ // Simple OR query with feature disabled
+ verify("//*[@name = 'foo' or @name = 'bar']",
+ false,
+ "select ... where [name] in('foo', 'bar') ");
+ }
+
+ @Test
+ public void testSimpleOrQueryFeatureEnabled() throws ParseException {
+ // Simple OR query with feature enabled
+ verify("//*[@name = 'foo' or @name = 'bar']",
+ true,
+ "select ... where [name] in('foo', 'bar') ");
+ }
+
+ @Test
+ public void testComplexNestedOrFeatureDisabled() throws ParseException {
+ // Complex nested query: (a AND (b OR c)) OR d
+ verify("//*[(@a = 1 and (@b = 2 or @c = 3)) or @d = 4]",
+ false,
+ "select ... where [a] = 1 and [b] = 2 union select ... where
[a] = 1 and [c] = 3 union select ... where [d] = 4 ");
+ }
+
+ @Test
+ public void testComplexNestedOrFeatureEnabled() throws ParseException {
+ // Complex nested query: (a AND (b OR c)) OR d
+ verify("//*[(@a = 1 and (@b = 2 or @c = 3)) or @d = 4]",
+ true,
+ "select ... where [a] = 1 and ([b] = 2 or [c] = 3) or [d] = 4
");
+ }
+
+ @Test
+ public void testOrWithDifferentPathsFeatureDisabled() throws
ParseException {
+ // Test OR conditions with different paths
+ verify("//*[@title = 'Test' or @desc = 'Test']",
+ false,
+ "select ... where [title] = 'Test' union select ... where
[desc] = 'Test' ");
+ }
+
+ @Test
+ public void testOrWithDifferentPathsFeatureEnabled() throws ParseException
{
+ // Test OR conditions with different paths
+ verify("//*[@title = 'Test' or @desc = 'Test']",
+ true,
+ "select ... where [title] = 'Test' or [desc] = 'Test' ");
+ }
+
+ @Test
+ public void testOrWithFunctionCallFeatureDisabled() throws ParseException {
+ // Test OR with function calls
+ verify("//*[jcr:contains(., 'test') or @type = 'page']",
+ false,
+ "select ... where contains(*, 'test') union select ... where
[type] = 'page' ");
+ }
+
+ @Test
+ public void testOrWithFunctionCallFeatureEnabled() throws ParseException {
+ verify("//*[jcr:contains(., 'test') or @type = 'page']",
+ true,
+ "select ... where contains(*, 'test') or [type] = 'page' ");
+ }
+
+ /**
+ * Helper method to create a Feature mock with the specified enabled state.
+ */
+ private static Feature createFeature(boolean enabled) {
+ Feature feature = Mockito.mock(Feature.class);
+ Mockito.when(feature.isEnabled()).thenReturn(enabled);
+ return feature;
+ }
+
+ private void verify(String xpath, boolean optimizeXPathUnion, String
expectedSql2) throws ParseException {
+ Feature feature = createFeature(optimizeXPathUnion);
+ QueryEngineSettings settings = new QueryEngineSettings();
+ settings.setOptimizeXPathUnion(feature);
+ XPathToSQL2Converter converter = new XPathToSQL2Converter(settings);
+ String sql2 = converter.convert(xpath);
+ sql2 = formatSQL(sql2);
+ assertEquals(expectedSql2, sql2);
+ }
+
+ static String formatSQL(String sql) {
+ sql = sql.replace('\n', ' ');
+ sql = sql.replaceAll("\\[jcr:path\\], \\[jcr:score\\], \\* from
\\[nt:base\\] as a", "...");
+ sql = sql.replaceAll("\\/\\*.*\\*/", "");
+ return sql;
+ }
+}
+