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

Reply via email to