diff --git a/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/KqlParserTests.java b/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/KqlParserTests.java index 1c8b1d127e8ed..3b352da0cd88e 100644 --- a/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/KqlParserTests.java +++ b/x-pack/plugin/kql/src/test/java/org/elasticsearch/xpack/kql/parser/KqlParserTests.java @@ -7,11 +7,21 @@ package org.elasticsearch.xpack.kql.parser; +import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.test.AbstractBuilderTestCase; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.isA; @@ -24,6 +34,32 @@ public void testEmptyQueryParsing() throws IOException { assertThat(parser.parseKqlQuery("", searchExecutionContext), isA(MatchAllQueryBuilder.class)); } + public void testSupportedQueries() throws Exception { + KqlParser parser = new KqlParser(); + SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); + + for (String query : readQueries("/supported-queries")) { + try { + parser.parseKqlQuery(query, searchExecutionContext); + } catch (Throwable e) { + throw new AssertionError("Unexpected error during query parsing [ " + query + "]", e); + } + } + } + + public void testUnsupportedQueries() throws Exception { + KqlParser parser = new KqlParser(); + SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); + + for (String query : readQueries("/unsupported-queries")) { + assertThrows( + "Was expecting a KqlParsingException exception to be thrown while parsing query [" + query + "]", + KqlParsingException.class, + () -> parser.parseKqlQuery(query, searchExecutionContext) + ); + } + } + public void testSyntaxErrorsHandling() throws IOException { KqlParser parser = new KqlParser(); @@ -49,4 +85,32 @@ public void testSyntaxErrorsHandling() throws IOException { assertThat(e.getMessage(), equalTo("line 1:15: missing ')' at 'AND'")); } } + + private static List readQueries(String source) throws Exception { + URL url = KqlParserTests.class.getResource(source); + Objects.requireNonNull(source, "Cannot find resource " + url); + + List queries = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(readFromJarUrl(url), StandardCharsets.UTF_8))) { + String line; + + while ((line = reader.readLine()) != null) { + String query = line.trim(); + // ignore comments + if (query.isEmpty() == false && query.startsWith("//") == false) { + queries.add(line.trim()); + } + } + } + return queries; + } + + @SuppressForbidden(reason = "test reads from jar") + private static InputStream readFromJarUrl(URL source) throws IOException { + URLConnection con = source.openConnection(); + // do not to cache files (to avoid keeping file handles around) + con.setUseCaches(false); + return con.getInputStream(); + } } diff --git a/x-pack/plugin/kql/src/test/resources/supported-queries b/x-pack/plugin/kql/src/test/resources/supported-queries new file mode 100644 index 0000000000000..d750f16149112 --- /dev/null +++ b/x-pack/plugin/kql/src/test/resources/supported-queries @@ -0,0 +1,111 @@ +// Match all queries +* +*:* +(*:*) + +// Queries with no field +200 +foo +foo bar +(foo bar) +foo* +*foo +f*oo +"foo bar" + +// Queries with all fields + *:200 + *:foo + *:foo bar + *:foo* + *:f*oo + *: *foo + *:"foo bar" + +// Querying a field +foo_field:200 +foo_field:foo +foo_field:foo bar +foo_field:(foo bar) +foo_field:foo* +foo_field: f*oo +foo_field: *foo +foo_field:"foo bar" +foo_field.subfield:foo +foo_*_field:foo +foo_field:* +foo_*:* + +// Range queries +foo_field<200 +foo_field=200 +foo_field>=foo +foo_field>"foo bar" +foo_field<=foo +foo_field>=foo + +// Boolean queries +NOT foo +NOT foo bar +NOT foo_field:foo +NOT foo_fieldbar +(foo_field:foo) AND (foo_field:foo bar) +foo_field:foo OR foo_field:foo bar +NOT(foo_field:foo OR foo_field:foo bar) +NOT(foo_field:foo AND foo_field:foo bar) +NOT foo_field:foo AND NOT foo_field:foo bar +(NOT foo_field:foo) AND (NOT foo_field:foo bar) +NOT(foo_field:foo) AND NOT(foo_field:foo bar) +foo_field:foo AND foo_field:foo bar AND foo bar +foo_field:foo AND foo_field:foo bar OR foo bar +foo_field:foo OR foo_field:foo bar OR foo bar +foo_field:foo OR foo_field:foo bar AND foo bar +foo_field:foo AND (foo_field:foo bar OR foo bar) +foo_field:foo AND (foo_field:foo bar OR foo bar) +foo_field:foo OR (foo_field:foo bar OR foo bar) + +// Nested queries +nested_field: { NOT foo } +nested_field: { NOT foo bar } +nested_field: { NOT foo_field:foo } +nested_field: { foo_field:foo AND foo_field:foo bar } +nested_field: { foo_fieldbar } +nested_field: { (foo_field:foo) AND (foo_field:foo bar) } +nested_field: { foo_field:foo OR foo_field:foo bar } +nested_field: { NOT(foo_field:foo OR foo_field:foo bar) } +nested_field: { NOT(foo_field:foo AND foo_field:foo bar) } +nested_field: { NOT foo_field:foo AND NOT foo_field:foo bar } +nested_field: { (NOT foo_field:foo) AND (NOT foo_field:foo bar) } +nested_field: { NOT(foo_field:foo) AND NOT(foo_field:foo bar) } +nested_field: { foo_field:foo AND foo_field:foo bar AND foo bar } +nested_field: { foo_field:foo AND foo_field:foo bar OR foo bar } +nested_field: { foo_field:foo OR foo_field:foo bar OR foo bar } +nested_field: { foo_field:foo OR foo_field:foo bar AND foo bar } +nested_field: { foo_field:foo AND (foo_field:foo bar OR foo bar) } +nested_field: { foo_field:foo AND (foo_field:foo bar OR foo bar) } +nested_field: { foo_field:foo OR (foo_field:foo bar OR foo bar) } +nested_field: { sub_nested_field : { foo_field:foo } AND foo_field:foo bar } + +// Queries with escape sequences +foo_field : (foo\(bar\)) +foo_field : foo\:bar +foo_field : (foo \and bar) +foo_field : (foo \or bar) +foo_field : foo \not bar +foo_field : foo \{bar\} +foo_field : foo \(bar\) +foo_field : foo \\ bar +foo_field : foo \"bar\" + +foo_field : "foo and bar" +foo_field : "foo not bar" +foo_field : "foo or bar" +foo_field : "foo : bar" +foo_field : "foo { bar }" +foo_field : "foo (bar)" +foo_field : "foo \\ bar" +foo_field : "foo \"bar\"" diff --git a/x-pack/plugin/kql/src/test/resources/unsupported-queries b/x-pack/plugin/kql/src/test/resources/unsupported-queries new file mode 100644 index 0000000000000..31e1544863bfb --- /dev/null +++ b/x-pack/plugin/kql/src/test/resources/unsupported-queries @@ -0,0 +1,42 @@ + +// Incomplete expressions +foo_field : +foo_field < +foo_field > +foo_field >= +foo_field <= + +// Parentheses mismatch +foo_field: (foo bar +foo_field: foo bar) +NOT foo_field:foo OR foo_field:foo bar) +NOT (foo_field:foo AND) foo_field:foo bar + +// Quotes mismatch +foo_field: "foo bar +foo_field: foo bar" + + +// Invalid boolean queries +foo AND +AND foo +foo OR +OR foo +NOT foo: + +// Can't nest grouping terms parentheses +foo_field:(foo (bar)) + +// Bad syntax for nested fields: +nested_field { foo: bar } + +// Missing escape sequences: +foo_field: foo:bar +foo_field: (foo and bar) +foo_field: (foo or bar) +foo_field: foo not bar +foo_field: foo { bar } +foo_field: foo (bar) +foo_field: foo "bar" +foo_field: "foo\ bar" +foo_field: "foo "bar""