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

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


The following commit(s) were added to refs/heads/OAK-10527 by this push:
     new 711c32cffd OAK-10527 Improve readability of the explain query output
711c32cffd is described below

commit 711c32cffd5283c1446326824ea2f4f5019bac97
Author: Thomas Mueller <[email protected]>
AuthorDate: Fri Nov 3 14:35:40 2023 +0100

    OAK-10527 Improve readability of the explain query output
---
 .../jackrabbit/oak/query/QueryFormatter.java       |  85 +++++++++++-----
 .../oak/jcr/query/QueryFormatterTest.java          | 108 ++++++++++++++++++---
 2 files changed, 152 insertions(+), 41 deletions(-)

diff --git 
a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryFormatter.java 
b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryFormatter.java
index 3a2cd811b8..7f73da0f1e 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryFormatter.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/QueryFormatter.java
@@ -23,15 +23,17 @@ import java.util.Locale;
 /**
  * Formatter for JCR queries in order to make them easier to read. Formatting 
is
  * done on a best-effort basis.
- * 
- * Warning: formatting is also done within e.g. string literals. So there is no
- * guarantee that the formatted query is semantically equal to the original 
one!
+ *
+ * Warning: Care was taken to not add newlines inside string literals and so 
on,
+ * but there is still no guarantee that the formatted query is semantically
+ * equal to the original one. It is recommended to run queries that are 
returned
+ * by these methods.
  */
 public class QueryFormatter {
 
     /**
      * Detect whether the query is an XPath query.
-     * 
+     *
      * @param query the query
      * @param language the language, if known, or null
      * @return true if xpath
@@ -46,7 +48,7 @@ public class QueryFormatter {
             query = query.substring("explain".length()).trim();
             if (query.startsWith("measure")) {
                 query = query.substring("explain".length()).trim();
-            }            
+            }
         }
         // union queries
         while (query.startsWith("(")) {
@@ -61,7 +63,7 @@ public class QueryFormatter {
     /**
      * Format the query into a more human-readable way, by adding newlines.
      * Warning: newlines are also added inside e.g. string literals.
-     * 
+     *
      * @param query the query (may not be null)
      * @param language the query language, or null if unknown
      * @return the formatted query
@@ -76,28 +78,61 @@ public class QueryFormatter {
     }
 
     private static String formatXPath(String query) {
-        query = query.replaceAll("\\[", "\\[\n  ");
-        for (String term : new String[] {
-                "and ", "or ", "order by ", "option\\("
-        }) {
-            // xpath is case sensitive
-            query = query.replaceAll(" (" + term + ")", "\n  $1");
+        StringBuilder buff = new StringBuilder(query);
+        for (int i = 0; i < buff.length(); i++) {
+            char c = buff.charAt(i);
+            if (c == '\'' || c == '"') {
+                while (++i < buff.length() && buff.charAt(i) != c) {
+                    // skip
+                }
+            } else if (c =='[') {
+                buff.insert(i + 1, "\n  ");
+                i += 3;
+            } else if (c <= ' ') {
+                String sub = buff.substring(i, Math.min(i + 10, 
buff.length()));
+                if (sub.startsWith(" and ")
+                        || sub.startsWith(" or ")
+                        || sub.startsWith(" order by ")
+                        || sub.startsWith(" option(")) {
+                    buff.setCharAt(i, '\n');
+                    buff.insert(i + 1, "  ");
+                    // just skip over the whitespace - but that's OK
+                    i += 2;
+                }
+            }
         }
-        // remove duplicate newlines
-        query = query.replaceAll("\n+", "\n");
-        return query;
+        return buff.toString();
     }
-    
+
     private static String formatSQL(String query) {
-        for (String term : new String[] {
-                "union ", "from ", "where ", "and ", "or ", "order by ", 
"option\\("
-        }) {
-            // SQL is case insensitive, so we use (?i)
-            query = query.replaceAll("(?i) (" + term + ")", "\n  $1");
+        StringBuilder buff = new StringBuilder(query);
+        for (int i = 0; i < buff.length(); i++) {
+            char c = buff.charAt(i);
+            if (c == '\'' || c == '"') {
+                while (++i < buff.length() && buff.charAt(i) != c) {
+                    // skip
+                }
+            } else if (c <= ' ') {
+                String sub = buff.substring(i, Math.min(i + 10, 
buff.length()));
+                if (startsWithIgnoreCase(sub, " and ")
+                        || startsWithIgnoreCase(sub, " or ")
+                        || startsWithIgnoreCase(sub, " union ")
+                        || startsWithIgnoreCase(sub, " from ")
+                        || startsWithIgnoreCase(sub, " where ")
+                        || startsWithIgnoreCase(sub, " order by ")
+                        || startsWithIgnoreCase(sub, " option(")) {
+                    buff.setCharAt(i, '\n');
+                    buff.insert(i + 1, "  ");
+                    // just skip over the whitespace - but that's OK
+                    i += 2;
+                }
+            }
         }
-        // remove duplicate newlines
-        query = query.replaceAll("\n+", "\n");
-        return query;
+        return buff.toString();
     }
-    
+
+    private static boolean startsWithIgnoreCase(String s, String prefix) {
+        return s.regionMatches(true, 0, prefix, 0, prefix.length());
+    }
+
 }
diff --git 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFormatterTest.java
 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFormatterTest.java
index bf9fba4dce..bd7417bf89 100644
--- 
a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFormatterTest.java
+++ 
b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/jcr/query/QueryFormatterTest.java
@@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import java.util.Random;
+
 import org.apache.jackrabbit.oak.query.QueryFormatter;
 import org.junit.Test;
 
@@ -31,47 +33,121 @@ public class QueryFormatterTest {
         assertFalse(QueryFormatter.isXPath("  select * from [nt:base]", null));
         assertFalse(QueryFormatter.isXPath("EXPLAIN SELECT ...", null));
         assertFalse(QueryFormatter.isXPath("explain measure  SELECT ...", 
null));
-        
+
         // common xpath
         assertTrue(QueryFormatter.isXPath("/jcr:root//*", null));
         assertTrue(QueryFormatter.isXPath(" /jcr:root//*", null));
         assertTrue(QueryFormatter.isXPath("\nexplain  
/jcr:root//element(*,rep:ACE)", null));
-        
+
         // xpath union
         assertTrue(QueryFormatter.isXPath("( ( /jcr:root//a | /jcr:root//b ) 
)", null));
-        
+
         // language is set explicitly
         assertTrue(QueryFormatter.isXPath("select", "xpath"));
     }
-    
+
+    @Test
+    public void formatRandomized() {
+        Random r = new Random(1);
+        for (int i = 0; i < 100000; i++) {
+            int len = r.nextInt(30);
+            StringBuilder buff = new StringBuilder();
+            for (int j = 0; j < len; j++) {
+                switch (r.nextInt(15)) {
+                case 0:
+                    buff.append('\'');
+                    break;
+                case 1:
+                    buff.append('\"');
+                    break;
+                case 2:
+                    buff.append('\n');
+                    break;
+                case 3:
+                    buff.append("or");
+                    break;
+                case 4:
+                    buff.append("and");
+                    break;
+                case 5:
+                    buff.append("from");
+                    break;
+                case 6:
+                    buff.append("order by");
+                    break;
+                case 7:
+                    buff.append("option");
+                    break;
+                case 8:
+                    buff.append('(');
+                    break;
+                case 9:
+                    buff.append('[');
+                    break;
+                default:
+                    buff.append(' ');
+                    break;
+                }
+            }
+            String query = buff.toString();
+            QueryFormatter.format(query, "xpath");
+            QueryFormatter.format(query, "sql");
+        }
+    }
+
     @Test
     public void format() {
         assertEquals("/jcr:root//*[\n"
                 + "  @a=1\n"
-                + "  and @b=2]\n"
-                + "  order by @c\n"
-                + "  option(traversal ok)", 
+                + "  and @b=2\n"
+                + "  or @c=3]\n"
+                + "  order by @d\n"
+                + "  option(traversal ok)",
                 QueryFormatter.format(
-                        "/jcr:root//*[@a=1 and @b=2] order by @c 
option(traversal ok)", null));
+                        "/jcr:root//*[@a=1 and @b=2 or @c=3] order by @d 
option(traversal ok)", null));
         assertEquals(
                 "sElEct *\n"
                 + "  FROM nt:base\n"
                 + "  WHERE x=1\n"
-                + "  and y=2",
+                + "  and y=2\n"
+                + "  Or z=3",
                 QueryFormatter.format(
-                        "sElEct * FROM nt:base WHERE x=1 and y=2",
+                        "sElEct * FROM nt:base WHERE x=1 and y=2 Or z=3",
                         null));
         assertEquals(
                 "select ...\n"
-                + "  union select ...",
+                + "  union select ...\n"
+                + "  order by '...",
+                QueryFormatter.format(
+                        "select ... union select ... order by '...",
+                        null));
+        assertEquals(
+                "select ' from  '' union '\n"
+                + "  from ...\n"
+                + "  option(...)",
+                QueryFormatter.format(
+                        "select ' from  '' union ' from ... option(...)",
+                        null));
+        assertEquals(
+                "select \" from  \"\" union \"\n"
+                + "  from ...\n"
+                + "  option(...)",
                 QueryFormatter.format(
-                        "select ... union select ...",
+                        "select \" from  \"\" union \" from ... option(...)",
                         null));
-        // formatting is also done inside string literals
         assertEquals(
                 "/jcr:root//*[\n"
-                + "  @x='\n"
-                + "  and ']",
-                QueryFormatter.format("/jcr:root//*[@x=' and ']", null));
+                + "  @x=' and '' and '\n"
+                + "  or @y=\" or \"]\n"
+                + "  order by @z",
+                QueryFormatter.format("/jcr:root//*[@x=' and '' and ' or @y=\" 
or \"] order by @z", null));
+        assertEquals(
+                "/jcr:root//*[\n"
+                + "  ",
+                QueryFormatter.format("/jcr:root//*[", null));
+        assertEquals(
+                "/jcr:root//*[\n"
+                + "  @a='",
+                QueryFormatter.format("/jcr:root//*[@a='", null));
     }
 }

Reply via email to