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