SQL: Implement sorting and retrieving score (elastic/x-pack-elasticsearch#3340)

Accessing `_score` from SQL looks like:
```
--------------------------------------------------
POST /_sql
{
    "query": "SELECT SCORE(), * FROM library WHERE match(name, 'dune') ORDER BY SCORE() DESC"
}
--------------------------------------------------
```

This replaces elastic/x-pack-elasticsearch#3187

relates elastic/x-pack-elasticsearch#2887

Original commit: elastic/x-pack-elasticsearch@fe96348c22
This commit is contained in:
Nik Everett 2017-12-18 20:57:50 -05:00 committed by GitHub
parent dc69f92b49
commit 4820bc757e
42 changed files with 1079 additions and 262 deletions

View File

@ -213,9 +213,15 @@ setups['library'] = '''
book: book:
properties: properties:
name: name:
type: keyword type: text
fields:
keyword:
type: keyword
author: author:
type: keyword type: text
fields:
keyword:
type: keyword
release_date: release_date:
type: date type: date
page_count: page_count:
@ -232,6 +238,12 @@ setups['library'] = '''
{"name": "Hyperion", "author": "Dan Simmons", "release_date": "1989-05-26", "page_count": 482} {"name": "Hyperion", "author": "Dan Simmons", "release_date": "1989-05-26", "page_count": 482}
{"index":{"_id": "Dune"}} {"index":{"_id": "Dune"}}
{"name": "Dune", "author": "Frank Herbert", "release_date": "1965-06-01", "page_count": 604} {"name": "Dune", "author": "Frank Herbert", "release_date": "1965-06-01", "page_count": 604}
{"index":{"_id": "Dune Messiah"}}
{"name": "Dune Messiah", "author": "Frank Herbert", "release_date": "1969-10-15", "page_count": 331}
{"index":{"_id": "Children of Dune"}}
{"name": "Children of Dune", "author": "Frank Herbert", "release_date": "1976-04-21", "page_count": 408}
{"index":{"_id": "God Emperor of Dune"}}
{"name": "God Emperor of Dune", "author": "Frank Herbert", "release_date": "1981-05-28", "page_count": 454}
{"index":{"_id": "Consider Phlebas"}} {"index":{"_id": "Consider Phlebas"}}
{"name": "Consider Phlebas", "author": "Iain M. Banks", "release_date": "1987-04-23", "page_count": 471} {"name": "Consider Phlebas", "author": "Iain M. Banks", "release_date": "1987-04-23", "page_count": 471}
{"index":{"_id": "Pandora's Star"}} {"index":{"_id": "Pandora's Star"}}

View File

@ -28,6 +28,7 @@ Frank Herbert |Dune |604 |-144720000000
Alastair Reynolds|Revelation Space |585 |953078400000 Alastair Reynolds|Revelation Space |585 |953078400000
James S.A. Corey |Leviathan Wakes |561 |1306972800000 James S.A. Corey |Leviathan Wakes |561 |1306972800000
-------------------------------------------------- --------------------------------------------------
// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/]
// TESTRESPONSE[_cat] // TESTRESPONSE[_cat]
You can also choose to get results in a structured format by adding the `format` parameter. Currently supported formats: You can also choose to get results in a structured format by adding the `format` parameter. Currently supported formats:
@ -58,8 +59,8 @@ Which returns:
-------------------------------------------------- --------------------------------------------------
{ {
"columns": [ "columns": [
{"name": "author", "type": "keyword"}, {"name": "author", "type": "text"},
{"name": "name", "type": "keyword"}, {"name": "name", "type": "text"},
{"name": "page_count", "type": "short"}, {"name": "page_count", "type": "short"},
{"name": "release_date", "type": "date"} {"name": "release_date", "type": "date"}
], ],
@ -100,8 +101,8 @@ Which looks like:
["Dan Simmons", "Hyperion", 482, 612144000000], ["Dan Simmons", "Hyperion", 482, 612144000000],
["Iain M. Banks", "Consider Phlebas", 471, 546134400000], ["Iain M. Banks", "Consider Phlebas", 471, 546134400000],
["Neal Stephenson", "Snow Crash", 470, 707356800000], ["Neal Stephenson", "Snow Crash", 470, 707356800000],
["Robert A. Heinlein", "Starship Troopers", 335, -318297600000], ["Frank Herbert", "God Emperor of Dune", 454, 359856000000],
["George Orwell", "1984", 328, 486432000000] ["Frank Herbert", "Children of Dune", 408, 198892800000]
], ],
"cursor" : "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWODRMaXBUaVlRN21iTlRyWHZWYUdrdw==:BAFmBmF1dGhvcgFmBG5hbWUBZgpwYWdlX2NvdW50AWYMcmVsZWFzZV9kYXRl9f///w8=" "cursor" : "sDXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAAEWODRMaXBUaVlRN21iTlRyWHZWYUdrdw==:BAFmBmF1dGhvcgFmBG5hbWUBZgpwYWdlX2NvdW50AWYMcmVsZWFzZV9kYXRl9f///w8="
} }
@ -174,6 +175,7 @@ Which returns:
---------------+------------------------------------+---------------+--------------- ---------------+------------------------------------+---------------+---------------
Douglas Adams |The Hitchhiker's Guide to the Galaxy|180 |308534400000 Douglas Adams |The Hitchhiker's Guide to the Galaxy|180 |308534400000
-------------------------------------------------- --------------------------------------------------
// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/]
// TESTRESPONSE[_cat] // TESTRESPONSE[_cat]
[[sql-rest-fields]] [[sql-rest-fields]]

View File

@ -23,11 +23,16 @@ Which returns:
{ {
"size" : 10, "size" : 10,
"docvalue_fields" : [ "docvalue_fields" : [
"author",
"name",
"page_count", "page_count",
"release_date" "release_date"
], ],
"_source": {
"includes": [
"author",
"name"
],
"excludes": []
},
"sort" : [ "sort" : [
{ {
"page_count" : { "page_count" : {

View File

@ -9,3 +9,92 @@ Each entry might get its own file and code snippet
-------------------------------------------------- --------------------------------------------------
include-tagged::{sql-spec}/select.sql-spec[wildcardWithOrder] include-tagged::{sql-spec}/select.sql-spec[wildcardWithOrder]
-------------------------------------------------- --------------------------------------------------
[[sql-spec-syntax-order-by]]
==== ORDER BY
Elasticsearch supports `ORDER BY` for consistent ordering. You add
any field in the index that has <<doc-values,`doc_values`>> or
`SCORE()` to sort by `_score`. By default SQL sorts on what it
considers to be the most efficient way to get the results.
So sorting by a field looks like:
[source,js]
--------------------------------------------------
POST /_xpack/sql
{
"query": "SELECT * FROM library ORDER BY page_count DESC LIMIT 5"
}
--------------------------------------------------
// CONSOLE
// TEST[setup:library]
which results in something like:
[source,text]
--------------------------------------------------
author | name | page_count | release_date
-----------------+--------------------+---------------+---------------
Peter F. Hamilton|Pandora's Star |768 |1078185600000
Vernor Vinge |A Fire Upon the Deep|613 |707356800000
Frank Herbert |Dune |604 |-144720000000
Alastair Reynolds|Revelation Space |585 |953078400000
James S.A. Corey |Leviathan Wakes |561 |1306972800000
--------------------------------------------------
// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/]
// TESTRESPONSE[_cat]
[[sql-spec-syntax-order-by-score]]
For sorting by score to be meaningful you need to include a full
text query in the `WHERE` clause. For example:
[source,js]
--------------------------------------------------
POST /_xpack/sql
{
"query": "SELECT SCORE(), * FROM library WHERE match(name, 'dune') ORDER BY SCORE() DESC"
}
--------------------------------------------------
// CONSOLE
// TEST[setup:library]
Which results in something like:
[source,text]
--------------------------------------------------
SCORE() | author | name | page_count | release_date
---------------+---------------+-------------------+---------------+---------------
2.288635 |Frank Herbert |Dune |604 |-144720000000
1.8893257 |Frank Herbert |Dune Messiah |331 |-6739200000
1.6086555 |Frank Herbert |Children of Dune |408 |198892800000
1.4005898 |Frank Herbert |God Emperor of Dune|454 |359856000000
--------------------------------------------------
// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/ s/\(/\\\(/ s/\)/\\\)/]
// TESTRESPONSE[_cat]
Note that you can return `SCORE()` by adding it to the where clause. This
is possible even if you are not sorting by `SCORE()`:
[source,js]
--------------------------------------------------
POST /_xpack/sql
{
"query": "SELECT SCORE(), * FROM library WHERE match(name, 'dune') ORDER BY page_count DESC"
}
--------------------------------------------------
// CONSOLE
// TEST[setup:library]
[source,text]
--------------------------------------------------
SCORE() | author | name | page_count | release_date
---------------+---------------+-------------------+---------------+---------------
2.288635 |Frank Herbert |Dune |604 |-144720000000
1.4005898 |Frank Herbert |God Emperor of Dune|454 |359856000000
1.6086555 |Frank Herbert |Children of Dune |408 |198892800000
1.8893257 |Frank Herbert |Dune Messiah |331 |-6739200000
--------------------------------------------------
// TESTRESPONSE[s/\|/\\|/ s/\+/\\+/ s/\(/\\\(/ s/\)/\\\)/]
// TESTRESPONSE[_cat]

View File

@ -45,7 +45,14 @@ public class CliExplainIT extends CliIntegrationTestCase {
assertThat(readLine(), startsWith(" \"test_field\"")); assertThat(readLine(), startsWith(" \"test_field\""));
assertThat(readLine(), startsWith(" ],")); assertThat(readLine(), startsWith(" ],"));
assertThat(readLine(), startsWith(" \"excludes\" : [ ]")); assertThat(readLine(), startsWith(" \"excludes\" : [ ]"));
assertThat(readLine(), startsWith(" }")); assertThat(readLine(), startsWith(" },"));
assertThat(readLine(), startsWith(" \"sort\" : ["));
assertThat(readLine(), startsWith(" {"));
assertThat(readLine(), startsWith(" \"_doc\" :"));
assertThat(readLine(), startsWith(" \"order\" : \"asc\""));
assertThat(readLine(), startsWith(" }"));
assertThat(readLine(), startsWith(" }"));
assertThat(readLine(), startsWith(" ]"));
assertThat(readLine(), startsWith("}]")); assertThat(readLine(), startsWith("}]"));
assertEquals("", readLine()); assertEquals("", readLine());
} }
@ -97,6 +104,13 @@ public class CliExplainIT extends CliIntegrationTestCase {
assertThat(readLine(), startsWith(" },")); assertThat(readLine(), startsWith(" },"));
assertThat(readLine(), startsWith(" \"docvalue_fields\" : [")); assertThat(readLine(), startsWith(" \"docvalue_fields\" : ["));
assertThat(readLine(), startsWith(" \"i\"")); assertThat(readLine(), startsWith(" \"i\""));
assertThat(readLine(), startsWith(" ],"));
assertThat(readLine(), startsWith(" \"sort\" : ["));
assertThat(readLine(), startsWith(" {"));
assertThat(readLine(), startsWith(" \"_doc\" :"));
assertThat(readLine(), startsWith(" \"order\" : \"asc\""));
assertThat(readLine(), startsWith(" }"));
assertThat(readLine(), startsWith(" }"));
assertThat(readLine(), startsWith(" ]")); assertThat(readLine(), startsWith(" ]"));
assertThat(readLine(), startsWith("}]")); assertThat(readLine(), startsWith("}]"));
assertEquals("", readLine()); assertEquals("", readLine());
@ -132,7 +146,14 @@ public class CliExplainIT extends CliIntegrationTestCase {
assertThat(readLine(), startsWith("EsQueryExec[test,{")); assertThat(readLine(), startsWith("EsQueryExec[test,{"));
assertThat(readLine(), startsWith(" \"size\" : 0,")); assertThat(readLine(), startsWith(" \"size\" : 0,"));
assertThat(readLine(), startsWith(" \"_source\" : false,")); assertThat(readLine(), startsWith(" \"_source\" : false,"));
assertThat(readLine(), startsWith(" \"stored_fields\" : \"_none_\"")); assertThat(readLine(), startsWith(" \"stored_fields\" : \"_none_\","));
assertThat(readLine(), startsWith(" \"sort\" : ["));
assertThat(readLine(), startsWith(" {"));
assertThat(readLine(), startsWith(" \"_doc\" :"));
assertThat(readLine(), startsWith(" \"order\" : \"asc\""));
assertThat(readLine(), startsWith(" }"));
assertThat(readLine(), startsWith(" }"));
assertThat(readLine(), startsWith(" ]"));
assertThat(readLine(), startsWith("}]")); assertThat(readLine(), startsWith("}]"));
assertEquals("", readLine()); assertEquals("", readLine());
} }

View File

@ -16,4 +16,9 @@ public interface ErrorsTestCase {
void testSelectFromIndexWithoutTypes() throws Exception; void testSelectFromIndexWithoutTypes() throws Exception;
void testSelectMissingField() throws Exception; void testSelectMissingField() throws Exception;
void testSelectMissingFunction() throws Exception; void testSelectMissingFunction() throws Exception;
void testSelectProjectScoreInAggContext() throws Exception;
void testSelectOrderByScoreInAggContext() throws Exception;
void testSelectGroupByScore() throws Exception;
void testSelectScoreSubField() throws Exception;
void testSelectScoreInScalar() throws Exception;
} }

View File

@ -11,6 +11,8 @@ import org.apache.http.entity.StringEntity;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.hamcrest.Matchers.startsWith;
/** /**
* Tests for error messages. * Tests for error messages.
*/ */
@ -49,4 +51,42 @@ public abstract class ErrorsTestCase extends CliIntegrationTestCase implements o
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT missing(foo) FROM test")); assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT missing(foo) FROM test"));
assertEquals("line 1:8: Unknown function [missing][1;23;31m][0m", readLine()); assertEquals("line 1:8: Unknown function [missing][1;23;31m][0m", readLine());
} }
@Override
public void testSelectProjectScoreInAggContext() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)", command("SELECT foo, SCORE(), COUNT(*) FROM test GROUP BY foo"));
assertEquals("line 1:13: Cannot use non-grouped column [SCORE()], expected [foo][1;23;31m][0m", readLine());
}
@Override
public void testSelectOrderByScoreInAggContext() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)",
command("SELECT foo, COUNT(*) FROM test GROUP BY foo ORDER BY SCORE()"));
assertEquals("line 1:54: Cannot order by non-grouped column [SCORE()], expected [foo][1;23;31m][0m", readLine());
}
@Override
public void testSelectGroupByScore() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)",
command("SELECT COUNT(*) FROM test GROUP BY SCORE()"));
assertEquals("line 1:36: Cannot use [SCORE()] for grouping[1;23;31m][0m", readLine());
}
@Override
public void testSelectScoreSubField() throws Exception {
index("test", body -> body.field("foo", 1));
assertThat(command("SELECT SCORE().bar FROM test"),
startsWith("[1;31mBad request [[22;3;33mline 1:15: extraneous input '.' expecting {<EOF>, ',',"));
}
@Override
public void testSelectScoreInScalar() throws Exception {
index("test", body -> body.field("foo", 1));
assertEquals("[1;31mBad request [[22;3;33mFound 1 problem(s)",
command("SELECT SIN(SCORE()) FROM test"));
assertEquals("line 1:12: [SCORE()] cannot be an argument to a function[1;23;31m][0m", readLine());
}
} }

View File

@ -39,9 +39,10 @@ public abstract class ShowTestCase extends CliIntegrationTestCase {
while (scalarFunction.matcher(line).matches()) { while (scalarFunction.matcher(line).matches()) {
line = readLine(); line = readLine();
} }
assertEquals("", line); assertThat(line, RegexMatcher.matches("\\s*SCORE\\s*\\|\\s*SCORE\\s*"));
assertEquals("", readLine());
} }
public void testShowFunctionsLikePrefix() throws IOException { public void testShowFunctionsLikePrefix() throws IOException {
assertThat(command("SHOW FUNCTIONS LIKE 'L%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); assertThat(command("SHOW FUNCTIONS LIKE 'L%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*"));
assertThat(readLine(), containsString("----------")); assertThat(readLine(), containsString("----------"));
@ -49,7 +50,7 @@ public abstract class ShowTestCase extends CliIntegrationTestCase {
assertThat(readLine(), RegexMatcher.matches("\\s*LOG10\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*LOG10\\s*\\|\\s*SCALAR\\s*"));
assertEquals("", readLine()); assertEquals("", readLine());
} }
public void testShowFunctionsLikeInfix() throws IOException { public void testShowFunctionsLikeInfix() throws IOException {
assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*"));
assertThat(readLine(), containsString("----------")); assertThat(readLine(), containsString("----------"));

View File

@ -12,6 +12,8 @@ import org.apache.http.entity.StringEntity;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.hamcrest.Matchers.startsWith;
/** /**
* Tests for exceptions and their messages. * Tests for exceptions and their messages.
*/ */
@ -60,4 +62,54 @@ public class ErrorsTestCase extends JdbcIntegrationTestCase implements org.elast
assertEquals("Found 1 problem(s)\nline 1:8: Unknown function [missing]", e.getMessage()); assertEquals("Found 1 problem(s)\nline 1:8: Unknown function [missing]", e.getMessage());
} }
} }
@Override
public void testSelectProjectScoreInAggContext() throws Exception {
index("test", body -> body.field("foo", 1));
try (Connection c = esJdbc()) {
SQLException e = expectThrows(SQLException.class, () ->
c.prepareStatement("SELECT foo, SCORE(), COUNT(*) FROM test GROUP BY foo").executeQuery());
assertEquals("Found 1 problem(s)\nline 1:13: Cannot use non-grouped column [SCORE()], expected [foo]", e.getMessage());
}
}
@Override
public void testSelectOrderByScoreInAggContext() throws Exception {
index("test", body -> body.field("foo", 1));
try (Connection c = esJdbc()) {
SQLException e = expectThrows(SQLException.class, () ->
c.prepareStatement("SELECT foo, COUNT(*) FROM test GROUP BY foo ORDER BY SCORE()").executeQuery());
assertEquals("Found 1 problem(s)\nline 1:54: Cannot order by non-grouped column [SCORE()], expected [foo]", e.getMessage());
}
}
@Override
public void testSelectGroupByScore() throws Exception {
index("test", body -> body.field("foo", 1));
try (Connection c = esJdbc()) {
SQLException e = expectThrows(SQLException.class, () ->
c.prepareStatement("SELECT COUNT(*) FROM test GROUP BY SCORE()").executeQuery());
assertEquals("Found 1 problem(s)\nline 1:36: Cannot use [SCORE()] for grouping", e.getMessage());
}
}
@Override
public void testSelectScoreSubField() throws Exception {
index("test", body -> body.field("foo", 1));
try (Connection c = esJdbc()) {
SQLException e = expectThrows(SQLException.class, () ->
c.prepareStatement("SELECT SCORE().bar FROM test").executeQuery());
assertThat(e.getMessage(), startsWith("line 1:15: extraneous input '.' expecting {<EOF>, ','"));
}
}
@Override
public void testSelectScoreInScalar() throws Exception {
index("test", body -> body.field("foo", 1));
try (Connection c = esJdbc()) {
SQLException e = expectThrows(SQLException.class, () ->
c.prepareStatement("SELECT SIN(SCORE()) FROM test").executeQuery());
assertThat(e.getMessage(), startsWith("Found 1 problem(s)\nline 1:12: [SCORE()] cannot be an argument to a function"));
}
}
} }

View File

@ -6,7 +6,7 @@
package org.elasticsearch.xpack.qa.sql.jdbc; package org.elasticsearch.xpack.qa.sql.jdbc;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.relique.jdbc.csv.CsvResultSet;
import java.sql.JDBCType; import java.sql.JDBCType;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.ResultSetMetaData; import java.sql.ResultSetMetaData;
@ -63,11 +63,11 @@ public class JdbcAssert {
expectedMeta.getColumnCount(), actualMeta.getColumnCount()), expectedMeta.getColumnCount(), actualMeta.getColumnCount()),
expectedCols.toString(), actualCols.toString()); expectedCols.toString(), actualCols.toString());
} }
for (int column = 1; column <= expectedMeta.getColumnCount(); column++) { for (int column = 1; column <= expectedMeta.getColumnCount(); column++) {
String expectedName = expectedMeta.getColumnName(column); String expectedName = expectedMeta.getColumnName(column);
String actualName = actualMeta.getColumnName(column); String actualName = actualMeta.getColumnName(column);
if (!expectedName.equals(actualName)) { if (!expectedName.equals(actualName)) {
// to help debugging, indicate the previous column (which also happened to match and thus was correct) // to help debugging, indicate the previous column (which also happened to match and thus was correct)
String expectedSet = expectedName; String expectedSet = expectedName;
@ -88,6 +88,10 @@ public class JdbcAssert {
if (expectedType == Types.TIMESTAMP_WITH_TIMEZONE) { if (expectedType == Types.TIMESTAMP_WITH_TIMEZONE) {
expectedType = Types.TIMESTAMP; expectedType = Types.TIMESTAMP;
} }
// since csv doesn't support real, we use float instead.....
if (expectedType == Types.FLOAT && expected instanceof CsvResultSet) {
expectedType = Types.REAL;
}
assertEquals("Different column type for column [" + expectedName + "] (" + JDBCType.valueOf(expectedType) + " != " assertEquals("Different column type for column [" + expectedName + "] (" + JDBCType.valueOf(expectedType) + " != "
+ JDBCType.valueOf(actualType) + ")", expectedType, actualType); + JDBCType.valueOf(actualType) + ")", expectedType, actualType);
} }
@ -136,4 +140,4 @@ public class JdbcAssert {
private static Object getTime(ResultSet rs, int column) throws SQLException { private static Object getTime(ResultSet rs, int column) throws SQLException {
return rs.getTime(column, UTC_CALENDAR).getTime(); return rs.getTime(column, UTC_CALENDAR).getTime();
} }
} }

View File

@ -10,6 +10,7 @@ import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity; import org.apache.http.entity.StringEntity;
import org.elasticsearch.client.Response; import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException; import org.elasticsearch.client.ResponseException;
import org.elasticsearch.common.CheckedSupplier;
import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.io.Streams; import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentHelper;
@ -71,7 +72,11 @@ public abstract class RestSqlTestCase extends ESRestTestCase implements ErrorsTe
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"), client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON)); new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
String request = "{\"query\":\"SELECT text, number, SIN(number) AS s FROM test ORDER BY number\", \"fetch_size\":2}"; String request = "{\"query\":\""
+ " SELECT text, number, SIN(number) AS s, SCORE()"
+ " FROM test"
+ " ORDER BY number, SCORE()\", "
+ "\"fetch_size\":2}";
String cursor = null; String cursor = null;
for (int i = 0; i < 20; i += 2) { for (int i = 0; i < 20; i += 2) {
@ -87,11 +92,12 @@ public abstract class RestSqlTestCase extends ESRestTestCase implements ErrorsTe
expected.put("columns", Arrays.asList( expected.put("columns", Arrays.asList(
columnInfo("text", "text"), columnInfo("text", "text"),
columnInfo("number", "long"), columnInfo("number", "long"),
columnInfo("s", "double"))); columnInfo("s", "double"),
columnInfo("SCORE()", "float")));
} }
expected.put("rows", Arrays.asList( expected.put("rows", Arrays.asList(
Arrays.asList("text" + i, i, Math.sin(i)), Arrays.asList("text" + i, i, Math.sin(i), 1.0),
Arrays.asList("text" + (i + 1), i + 1, Math.sin(i + 1)))); Arrays.asList("text" + (i + 1), i + 1, Math.sin(i + 1), 1.0)));
expected.put("size", 2); expected.put("size", 2);
cursor = (String) response.remove("cursor"); cursor = (String) response.remove("cursor");
assertResponse(expected, response); assertResponse(expected, response);
@ -118,6 +124,26 @@ public abstract class RestSqlTestCase extends ESRestTestCase implements ErrorsTe
new StringEntity("{\"query\":\"SELECT DAY_OF_YEAR(test), COUNT(*) FROM test\"}", ContentType.APPLICATION_JSON))); new StringEntity("{\"query\":\"SELECT DAY_OF_YEAR(test), COUNT(*) FROM test\"}", ContentType.APPLICATION_JSON)));
} }
public void testScoreWithFieldNamedScore() throws IOException {
StringBuilder bulk = new StringBuilder();
bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
bulk.append("{\"name\":\"test\", \"score\":10}\n");
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
Map<String, Object> expected = new HashMap<>();
expected.put("columns", Arrays.asList(
columnInfo("name", "text"),
columnInfo("score", "long"),
columnInfo("SCORE()", "float")));
expected.put("rows", singletonList(Arrays.asList(
"test", 10, 1.0)));
expected.put("size", 1);
assertResponse(expected, runSql("SELECT *, SCORE() FROM test ORDER BY SCORE()"));
assertResponse(expected, runSql("SELECT name, \\\"score\\\", SCORE() FROM test ORDER BY SCORE()"));
}
public void testSelectWithJoinFails() throws Exception { public void testSelectWithJoinFails() throws Exception {
// Normal join not supported // Normal join not supported
expectBadRequest(() -> runSql("SELECT * FROM test JOIN other"), expectBadRequest(() -> runSql("SELECT * FROM test JOIN other"),
@ -177,10 +203,92 @@ public abstract class RestSqlTestCase extends ESRestTestCase implements ErrorsTe
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON)); new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
} }
private void expectBadRequest(ThrowingRunnable code, Matcher<String> errorMessageMatcher) { @Override
ResponseException e = expectThrows(ResponseException.class, code); public void testSelectProjectScoreInAggContext() throws Exception {
assertEquals(e.getMessage(), 400, e.getResponse().getStatusLine().getStatusCode()); StringBuilder bulk = new StringBuilder();
assertThat(e.getMessage(), errorMessageMatcher); bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
bulk.append("{\"foo\":1}\n");
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
expectBadRequest(() -> runSql(
" SELECT foo, SCORE(), COUNT(*)"
+ " FROM test"
+ " GROUP BY foo"),
containsString("Cannot use non-grouped column [SCORE()], expected [foo]"));
}
@Override
public void testSelectOrderByScoreInAggContext() throws Exception {
StringBuilder bulk = new StringBuilder();
bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
bulk.append("{\"foo\":1}\n");
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
expectBadRequest(() -> runSql(
" SELECT foo, COUNT(*)"
+ " FROM test"
+ " GROUP BY foo"
+ " ORDER BY SCORE()"),
containsString("Cannot order by non-grouped column [SCORE()], expected [foo]"));
}
@Override
public void testSelectGroupByScore() throws Exception {
StringBuilder bulk = new StringBuilder();
bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
bulk.append("{\"foo\":1}\n");
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
expectBadRequest(() -> runSql("SELECT COUNT(*) FROM test GROUP BY SCORE()"),
containsString("Cannot use [SCORE()] for grouping"));
}
@Override
public void testSelectScoreSubField() throws Exception {
StringBuilder bulk = new StringBuilder();
bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
bulk.append("{\"foo\":1}\n");
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
expectBadRequest(() -> runSql("SELECT SCORE().bar FROM test"),
containsString("line 1:15: extraneous input '.' expecting {<EOF>, ','"));
}
@Override
public void testSelectScoreInScalar() throws Exception {
StringBuilder bulk = new StringBuilder();
bulk.append("{\"index\":{\"_id\":\"1\"}}\n");
bulk.append("{\"foo\":1}\n");
client().performRequest("POST", "/test/test/_bulk", singletonMap("refresh", "true"),
new StringEntity(bulk.toString(), ContentType.APPLICATION_JSON));
expectBadRequest(() -> runSql("SELECT SIN(SCORE()) FROM test"),
containsString("line 1:12: [SCORE()] cannot be an argument to a function"));
}
private void expectBadRequest(CheckedSupplier<Map<String, Object>, Exception> code, Matcher<String> errorMessageMatcher) {
try {
Map<String, Object> result = code.get();
fail("expected ResponseException but got " + result);
} catch (ResponseException e) {
if (400 != e.getResponse().getStatusLine().getStatusCode()) {
String body;
try {
body = Streams.copyToString(new InputStreamReader(
e.getResponse().getEntity().getContent(), StandardCharsets.UTF_8));
} catch (IOException bre) {
throw new RuntimeException("error reading body after remote sent bad status", bre);
}
fail("expected [400] response but get [" + e.getResponse().getStatusLine().getStatusCode() + "] with body:\n" + body);
}
assertThat(e.getMessage(), errorMessageMatcher);
} catch (Exception e) {
throw new AssertionError("expected ResponseException but got [" + e.getClass() + "]", e);
}
} }
private Map<String, Object> runSql(String sql) throws IOException { private Map<String, Object> runSql(String sql) throws IOException {
@ -278,13 +386,12 @@ public abstract class RestSqlTestCase extends ESRestTestCase implements ErrorsTe
"{\"test\":\"test\"}"); "{\"test\":\"test\"}");
String expected = String expected =
" test \n" +
"---------------\n" +
"test \n" + "test \n" +
"---------------\n" + "test \n";
"test \n" +
"test \n";
Tuple<String, String> response = runSqlAsText("SELECT * FROM test"); Tuple<String, String> response = runSqlAsText("SELECT * FROM test");
logger.warn(expected); assertEquals(expected, response.v1());
logger.warn(response.v1());
} }
public void testNextPageText() throws IOException { public void testNextPageText() throws IOException {

View File

@ -64,6 +64,7 @@ SIN |SCALAR
SINH |SCALAR SINH |SCALAR
SQRT |SCALAR SQRT |SCALAR
TAN |SCALAR TAN |SCALAR
SCORE |SCORE
; ;
showFunctionsWithExactMatch showFunctionsWithExactMatch

View File

@ -29,3 +29,17 @@ SELECT emp_no, first_name, gender, last_name FROM test_emp WHERE MATCH('first_na
emp_no:i | first_name:s | gender:s | last_name:s emp_no:i | first_name:s | gender:s | last_name:s
10095 |Hilari |M |Morton 10095 |Hilari |M |Morton
; ;
score
SELECT emp_no, first_name, SCORE() FROM test_emp WHERE MATCH(first_name, 'Erez') ORDER BY SCORE();
emp_no:i | first_name:s | SCORE():f
10076 |Erez |4.2096553
;
scoreAsSomething
SELECT emp_no, first_name, SCORE() as s FROM test_emp WHERE MATCH(first_name, 'Erez') ORDER BY SCORE();
emp_no:i | first_name:s | s:f
10076 |Erez |4.2096553
;

View File

@ -1013,4 +1013,4 @@ public class Analyzer extends RuleExecutor<LogicalPlan> {
return true; return true;
} }
} }
} }

View File

@ -6,13 +6,16 @@
package org.elasticsearch.xpack.sql.analysis.analyzer; package org.elasticsearch.xpack.sql.analysis.analyzer;
import org.elasticsearch.xpack.sql.capabilities.Unresolvable; import org.elasticsearch.xpack.sql.capabilities.Unresolvable;
import org.elasticsearch.xpack.sql.expression.Alias;
import org.elasticsearch.xpack.sql.expression.Attribute; import org.elasticsearch.xpack.sql.expression.Attribute;
import org.elasticsearch.xpack.sql.expression.Expression; import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.Expressions; import org.elasticsearch.xpack.sql.expression.Expressions;
import org.elasticsearch.xpack.sql.expression.Order;
import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute;
import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.expression.function.Function;
import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute;
import org.elasticsearch.xpack.sql.expression.function.Functions; import org.elasticsearch.xpack.sql.expression.function.Functions;
import org.elasticsearch.xpack.sql.expression.function.Score;
import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.sql.plan.logical.Aggregate; import org.elasticsearch.xpack.sql.plan.logical.Aggregate;
import org.elasticsearch.xpack.sql.plan.logical.Filter; import org.elasticsearch.xpack.sql.plan.logical.Filter;
@ -150,7 +153,6 @@ abstract class Verifier {
// Concrete verifications // Concrete verifications
//
// if there are no (major) unresolved failures, do more in-depth analysis // if there are no (major) unresolved failures, do more in-depth analysis
if (failures.isEmpty()) { if (failures.isEmpty()) {
@ -182,14 +184,17 @@ abstract class Verifier {
if (!groupingFailures.contains(p)) { if (!groupingFailures.contains(p)) {
checkGroupBy(p, localFailures, resolvedFunctions, groupingFailures); checkGroupBy(p, localFailures, resolvedFunctions, groupingFailures);
} }
// everything checks out
// mark the plan as analyzed
if (localFailures.isEmpty()) {
p.setAnalyzed();
}
failures.addAll(localFailures); checkForScoreInsideFunctions(p, localFailures);
});
// everything checks out
// mark the plan as analyzed
if (localFailures.isEmpty()) {
p.setAnalyzed();
}
failures.addAll(localFailures);
});
} }
return failures; return failures;
@ -256,6 +261,8 @@ abstract class Verifier {
} }
return true; return true;
} }
// check whether plain columns specified in an agg are mentioned in the group-by // check whether plain columns specified in an agg are mentioned in the group-by
private static boolean checkGroupByAgg(LogicalPlan p, Set<Failure> localFailures, Set<LogicalPlan> groupingFailures, Map<String, Function> functions) { private static boolean checkGroupByAgg(LogicalPlan p, Set<Failure> localFailures, Set<LogicalPlan> groupingFailures, Map<String, Function> functions) {
if (p instanceof Aggregate) { if (p instanceof Aggregate) {
@ -266,6 +273,9 @@ abstract class Verifier {
if (Functions.isAggregate(c)) { if (Functions.isAggregate(c)) {
localFailures.add(fail(c, "Cannot use an aggregate [" + c.nodeName().toUpperCase(Locale.ROOT) + "] for grouping")); localFailures.add(fail(c, "Cannot use an aggregate [" + c.nodeName().toUpperCase(Locale.ROOT) + "] for grouping"));
} }
if (c instanceof Score) {
localFailures.add(fail(c, "Cannot use [SCORE()] for grouping"));
}
})); }));
if (!localFailures.isEmpty()) { if (!localFailures.isEmpty()) {
@ -306,24 +316,30 @@ abstract class Verifier {
// TODO: this should be handled by a different rule // TODO: this should be handled by a different rule
if (function == null) { if (function == null) {
return false; return false;
}
e = function;
} }
e = function;
}
// scalar functions can be a binary tree // scalar functions can be a binary tree
// first test the function against the grouping // first test the function against the grouping
// and if that fails, start unpacking hoping to find matches // and if that fails, start unpacking hoping to find matches
if (e instanceof ScalarFunction) { if (e instanceof ScalarFunction) {
ScalarFunction sf = (ScalarFunction) e; ScalarFunction sf = (ScalarFunction) e;
// found group for the expression // found group for the expression
if (Expressions.anyMatch(groupings, e::semanticEquals)) { if (Expressions.anyMatch(groupings, e::semanticEquals)) {
return true; return true;
} }
// unwrap function to find the base // unwrap function to find the base
for (Expression arg : sf.arguments()) { for (Expression arg : sf.arguments()) {
arg.collectFirstChildren(c -> checkGroupMatch(c, source, groupings, missing, functions)); arg.collectFirstChildren(c -> checkGroupMatch(c, source, groupings, missing, functions));
} }
return true;
} else if (e instanceof Score) {
// Score can't be an aggretate function
missing.put(e, source);
return true; return true;
} }
@ -347,4 +363,14 @@ abstract class Verifier {
} }
return false; return false;
} }
}
private static void checkForScoreInsideFunctions(LogicalPlan p, Set<Failure> localFailures) {
// Make sure that SCORE is only used in "top level" functions
p.forEachExpressions(e ->
e.forEachUp((Function f) ->
f.arguments().stream()
.filter(exp -> exp.anyMatch(Score.class::isInstance))
.forEach(exp -> localFailures.add(fail(exp, "[SCORE()] cannot be an argument to a function"))),
Function.class));
}
}

View File

@ -26,13 +26,16 @@ import org.elasticsearch.xpack.sql.expression.NestedFieldAttribute;
import org.elasticsearch.xpack.sql.expression.RootFieldAttribute; import org.elasticsearch.xpack.sql.expression.RootFieldAttribute;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ReferenceInput; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ReferenceInput;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ScoreProcessorDefinition;
import org.elasticsearch.xpack.sql.querydsl.agg.Aggs; import org.elasticsearch.xpack.sql.querydsl.agg.Aggs;
import org.elasticsearch.xpack.sql.querydsl.agg.GroupByColumnAgg; import org.elasticsearch.xpack.sql.querydsl.agg.GroupByColumnAgg;
import org.elasticsearch.xpack.sql.querydsl.agg.GroupingAgg; import org.elasticsearch.xpack.sql.querydsl.agg.GroupingAgg;
import org.elasticsearch.xpack.sql.querydsl.container.AggRef;
import org.elasticsearch.xpack.sql.querydsl.container.AttributeSort; import org.elasticsearch.xpack.sql.querydsl.container.AttributeSort;
import org.elasticsearch.xpack.sql.querydsl.container.ColumnReference; import org.elasticsearch.xpack.sql.querydsl.container.ColumnReference;
import org.elasticsearch.xpack.sql.querydsl.container.ComputedRef; import org.elasticsearch.xpack.sql.querydsl.container.ComputedRef;
import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer; import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer;
import org.elasticsearch.xpack.sql.querydsl.container.ScoreSort;
import org.elasticsearch.xpack.sql.querydsl.container.ScriptFieldRef; import org.elasticsearch.xpack.sql.querydsl.container.ScriptFieldRef;
import org.elasticsearch.xpack.sql.querydsl.container.ScriptSort; import org.elasticsearch.xpack.sql.querydsl.container.ScriptSort;
import org.elasticsearch.xpack.sql.querydsl.container.SearchHitFieldRef; import org.elasticsearch.xpack.sql.querydsl.container.SearchHitFieldRef;
@ -50,6 +53,7 @@ import java.util.Set;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.elasticsearch.search.sort.SortBuilders.fieldSort; import static org.elasticsearch.search.sort.SortBuilders.fieldSort;
import static org.elasticsearch.search.sort.SortBuilders.scoreSort;
import static org.elasticsearch.search.sort.SortBuilders.scriptSort; import static org.elasticsearch.search.sort.SortBuilders.scriptSort;
public abstract class SourceGenerator { public abstract class SourceGenerator {
@ -77,7 +81,7 @@ public abstract class SourceGenerator {
Map<String, Script> scriptFields = new LinkedHashMap<>(); Map<String, Script> scriptFields = new LinkedHashMap<>();
for (ColumnReference ref : container.columns()) { for (ColumnReference ref : container.columns()) {
collectFields(ref, sourceFields, docFields, scriptFields); collectFields(source, ref, sourceFields, docFields, scriptFields);
} }
if (!sourceFields.isEmpty()) { if (!sourceFields.isEmpty()) {
@ -92,8 +96,6 @@ public abstract class SourceGenerator {
source.scriptField(entry.getKey(), entry.getValue()); source.scriptField(entry.getKey(), entry.getValue());
} }
sorting(container, source);
// add the aggs // add the aggs
Aggs aggs = container.aggs(); Aggs aggs = container.aggs();
@ -115,6 +117,8 @@ public abstract class SourceGenerator {
source.aggregation(builder); source.aggregation(builder);
} }
sorting(container, source);
// add the pipeline aggs // add the pipeline aggs
for (PipelineAggregationBuilder builder : aggs.asPipelineBuilders()) { for (PipelineAggregationBuilder builder : aggs.asPipelineBuilders()) {
source.aggregation(builder); source.aggregation(builder);
@ -133,97 +137,110 @@ public abstract class SourceGenerator {
return source; return source;
} }
private static void collectFields(ColumnReference ref, Set<String> sourceFields, Set<String> docFields, Map<String, Script> scriptFields) { private static void collectFields(SearchSourceBuilder source, ColumnReference ref,
Set<String> sourceFields, Set<String> docFields, Map<String, Script> scriptFields) {
if (ref instanceof ComputedRef) { if (ref instanceof ComputedRef) {
ProcessorDefinition proc = ((ComputedRef) ref).processor(); ProcessorDefinition proc = ((ComputedRef) ref).processor();
proc.forEachUp(l -> collectFields(l.context(), sourceFields, docFields, scriptFields), ReferenceInput.class); if (proc instanceof ScoreProcessorDefinition) {
} /*
else if (ref instanceof SearchHitFieldRef) { * If we're SELECTing SCORE then force tracking scores just in case
* we're not sorting on them.
*/
source.trackScores(true);
}
proc.forEachUp(l -> collectFields(source, l.context(), sourceFields, docFields, scriptFields), ReferenceInput.class);
} else if (ref instanceof SearchHitFieldRef) {
SearchHitFieldRef sh = (SearchHitFieldRef) ref; SearchHitFieldRef sh = (SearchHitFieldRef) ref;
Set<String> collection = sh.useDocValue() ? docFields : sourceFields; Set<String> collection = sh.useDocValue() ? docFields : sourceFields;
collection.add(sh.name()); collection.add(sh.name());
} } else if (ref instanceof ScriptFieldRef) {
else if (ref instanceof ScriptFieldRef) {
ScriptFieldRef sfr = (ScriptFieldRef) ref; ScriptFieldRef sfr = (ScriptFieldRef) ref;
scriptFields.put(sfr.name(), sfr.script().toPainless()); scriptFields.put(sfr.name(), sfr.script().toPainless());
} else if (ref instanceof AggRef) {
// Nothing to do
} else {
throw new IllegalStateException("unhandled field in collectFields [" + ref.getClass() + "][" + ref + "]");
} }
} }
private static void sorting(QueryContainer container, SearchSourceBuilder source) { private static void sorting(QueryContainer container, SearchSourceBuilder source) {
if (container.sort() != null) { if (source.aggregations() != null && source.aggregations().count() > 0) {
// Aggs can't be sorted using search sorting. That sorting is handled elsewhere.
for (Sort sortable : container.sort()) { return;
SortBuilder<?> sortBuilder = null;
if (sortable instanceof AttributeSort) {
AttributeSort as = (AttributeSort) sortable;
Attribute attr = as.attribute();
// sorting only works on not-analyzed fields - look for a multi-field replacement
if (attr instanceof FieldAttribute) {
FieldAttribute fa = (FieldAttribute) attr;
attr = fa.isAnalyzed() ? fa.notAnalyzedAttribute() : attr;
}
// top-level doc value
if (attr instanceof RootFieldAttribute) {
sortBuilder = fieldSort(((RootFieldAttribute) attr).name());
}
if (attr instanceof NestedFieldAttribute) {
NestedFieldAttribute nfa = (NestedFieldAttribute) attr;
FieldSortBuilder fieldSort = fieldSort(nfa.name());
String nestedPath = nfa.parentPath();
NestedSortBuilder newSort = new NestedSortBuilder(nestedPath);
NestedSortBuilder nestedSort = fieldSort.getNestedSort();
if (nestedSort == null) {
fieldSort.setNestedSort(newSort);
} else {
for (; nestedSort.getNestedSort() != null; nestedSort = nestedSort.getNestedSort()) {
}
nestedSort.setNestedSort(newSort);
}
nestedSort = newSort;
List<QueryBuilder> nestedQuery = new ArrayList<>(1);
// copy also the nested queries fr(if any)
if (container.query() != null) {
container.query().forEachDown(nq -> {
// found a match
if (nestedPath.equals(nq.path())) {
// get the child query - the nested wrapping and inner hits are not needed
nestedQuery.add(nq.child().asBuilder());
}
}, NestedQuery.class);
}
if (nestedQuery.size() > 0) {
if (nestedQuery.size() > 1) {
throw new SqlIllegalArgumentException("nested query should have been grouped in one place");
}
nestedSort.setFilter(nestedQuery.get(0));
}
sortBuilder = fieldSort;
}
}
if (sortable instanceof ScriptSort) {
ScriptSort ss = (ScriptSort) sortable;
sortBuilder = scriptSort(ss.script().toPainless(), ss.script().outputType().isNumeric() ? ScriptSortType.NUMBER : ScriptSortType.STRING);
}
if (sortBuilder != null) {
sortBuilder.order(sortable.direction() == Direction.ASC ? SortOrder.ASC : SortOrder.DESC);
source.sort(sortBuilder);
}
}
} }
else { if (container.sort() == null || container.sort().isEmpty()) {
// if no sorting is specified, use the _doc one // if no sorting is specified, use the _doc one
source.sort("_doc"); source.sort("_doc");
return;
}
for (Sort sortable : container.sort()) {
SortBuilder<?> sortBuilder = null;
if (sortable instanceof AttributeSort) {
AttributeSort as = (AttributeSort) sortable;
Attribute attr = as.attribute();
// sorting only works on not-analyzed fields - look for a multi-field replacement
if (attr instanceof FieldAttribute) {
FieldAttribute fa = (FieldAttribute) attr;
attr = fa.isAnalyzed() ? fa.notAnalyzedAttribute() : attr;
}
// top-level doc value
if (attr instanceof RootFieldAttribute) {
sortBuilder = fieldSort(((RootFieldAttribute) attr).name());
}
if (attr instanceof NestedFieldAttribute) {
NestedFieldAttribute nfa = (NestedFieldAttribute) attr;
FieldSortBuilder fieldSort = fieldSort(nfa.name());
String nestedPath = nfa.parentPath();
NestedSortBuilder newSort = new NestedSortBuilder(nestedPath);
NestedSortBuilder nestedSort = fieldSort.getNestedSort();
if (nestedSort == null) {
fieldSort.setNestedSort(newSort);
} else {
for (; nestedSort.getNestedSort() != null; nestedSort = nestedSort.getNestedSort()) {
}
nestedSort.setNestedSort(newSort);
}
nestedSort = newSort;
List<QueryBuilder> nestedQuery = new ArrayList<>(1);
// copy also the nested queries fr(if any)
if (container.query() != null) {
container.query().forEachDown(nq -> {
// found a match
if (nestedPath.equals(nq.path())) {
// get the child query - the nested wrapping and inner hits are not needed
nestedQuery.add(nq.child().asBuilder());
}
}, NestedQuery.class);
}
if (nestedQuery.size() > 0) {
if (nestedQuery.size() > 1) {
throw new SqlIllegalArgumentException("nested query should have been grouped in one place");
}
nestedSort.setFilter(nestedQuery.get(0));
}
sortBuilder = fieldSort;
}
} else if (sortable instanceof ScriptSort) {
ScriptSort ss = (ScriptSort) sortable;
sortBuilder = scriptSort(ss.script().toPainless(), ss.script().outputType().isNumeric() ? ScriptSortType.NUMBER : ScriptSortType.STRING);
} else if (sortable instanceof ScoreSort) {
sortBuilder = scoreSort();
}
if (sortBuilder != null) {
sortBuilder.order(sortable.direction() == Direction.ASC ? SortOrder.ASC : SortOrder.DESC);
source.sort(sortBuilder);
}
} }
} }
@ -236,4 +253,4 @@ public abstract class SourceGenerator {
source.storedFields(NO_STORED_FIELD); source.storedFields(NO_STORED_FIELD);
} }
} }
} }

View File

@ -20,12 +20,16 @@ import java.util.Objects;
* {@link Processor} tree as a leaf (and thus can effectively parse the * {@link Processor} tree as a leaf (and thus can effectively parse the
* {@link SearchHit} while this class is used when scrolling and passing down * {@link SearchHit} while this class is used when scrolling and passing down
* the results. * the results.
* *
* In the future, the processor might be used across the board for all columns * In the future, the processor might be used across the board for all columns
* to reduce API complexity (and keep the {@link HitExtractor} only as an * to reduce API complexity (and keep the {@link HitExtractor} only as an
* internal implementation detail). * internal implementation detail).
*/ */
public class ComputingHitExtractor implements HitExtractor { public class ComputingHitExtractor implements HitExtractor {
/**
* Stands for {@code comPuting}. We try to use short names for {@link HitExtractor}s
* to save a few bytes when when we send them back to the user.
*/
static final String NAME = "p"; static final String NAME = "p";
private final Processor processor; private final Processor processor;
@ -79,4 +83,4 @@ public class ComputingHitExtractor implements HitExtractor {
public String toString() { public String toString() {
return processor.toString(); return processor.toString();
} }
} }

View File

@ -16,6 +16,10 @@ import java.util.Objects;
* Returns the a constant for every search hit against which it is run. * Returns the a constant for every search hit against which it is run.
*/ */
public class ConstantExtractor implements HitExtractor { public class ConstantExtractor implements HitExtractor {
/**
* Stands for {@code constant}. We try to use short names for {@link HitExtractor}s
* to save a few bytes when when we send them back to the user.
*/
static final String NAME = "c"; static final String NAME = "c";
private final Object constant; private final Object constant;
@ -65,4 +69,4 @@ public class ConstantExtractor implements HitExtractor {
public String toString() { public String toString() {
return "^" + constant; return "^" + constant;
} }
} }

View File

@ -17,7 +17,11 @@ import java.io.IOException;
* Extracts field values from {@link SearchHit#field(String)}. * Extracts field values from {@link SearchHit#field(String)}.
*/ */
public class DocValueExtractor implements HitExtractor { public class DocValueExtractor implements HitExtractor {
static final String NAME = "f"; /**
* Stands for {@code doc_value}. We try to use short names for {@link HitExtractor}s
* to save a few bytes when when we send them back to the user.
*/
static final String NAME = "d";
private final String fieldName; private final String fieldName;
public DocValueExtractor(String name) { public DocValueExtractor(String name) {
@ -81,4 +85,4 @@ public class DocValueExtractor implements HitExtractor {
* values are. */ * values are. */
return "%" + fieldName; return "%" + fieldName;
} }
} }

View File

@ -13,7 +13,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public abstract class HitExtractors { public abstract class HitExtractors {
/** /**
* All of the named writeables needed to deserialize the instances of * All of the named writeables needed to deserialize the instances of
* {@linkplain HitExtractor}. * {@linkplain HitExtractor}.
@ -25,6 +24,7 @@ public abstract class HitExtractors {
entries.add(new Entry(HitExtractor.class, InnerHitExtractor.NAME, InnerHitExtractor::new)); entries.add(new Entry(HitExtractor.class, InnerHitExtractor.NAME, InnerHitExtractor::new));
entries.add(new Entry(HitExtractor.class, SourceExtractor.NAME, SourceExtractor::new)); entries.add(new Entry(HitExtractor.class, SourceExtractor.NAME, SourceExtractor::new));
entries.add(new Entry(HitExtractor.class, ComputingHitExtractor.NAME, ComputingHitExtractor::new)); entries.add(new Entry(HitExtractor.class, ComputingHitExtractor.NAME, ComputingHitExtractor::new));
entries.add(new Entry(HitExtractor.class, ScoreExtractor.NAME, in -> ScoreExtractor.INSTANCE));
entries.addAll(Processors.getNamedWriteables()); entries.addAll(Processors.getNamedWriteables());
return entries; return entries;
} }

View File

@ -17,6 +17,10 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
public class InnerHitExtractor implements HitExtractor { public class InnerHitExtractor implements HitExtractor {
/**
* Stands for {@code inner}. We try to use short names for {@link HitExtractor}s
* to save a few bytes when when we send them back to the user.
*/
static final String NAME = "i"; static final String NAME = "i";
private final String hitName, fieldName; private final String hitName, fieldName;
private final boolean useDocValue; private final boolean useDocValue;
@ -109,4 +113,4 @@ public class InnerHitExtractor implements HitExtractor {
public int hashCode() { public int hashCode() {
return Objects.hash(hitName, fieldName, useDocValue); return Objects.hash(hitName, fieldName, useDocValue);
} }
} }

View File

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.execution.search.extractor;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.search.SearchHit;
import java.io.IOException;
/**
* Returns the a constant for every search hit against which it is run.
*/
public class ScoreExtractor implements HitExtractor {
public static final HitExtractor INSTANCE = new ScoreExtractor();
/**
* Stands for {@code score}. We try to use short names for {@link HitExtractor}s
* to save a few bytes when when we send them back to the user.
*/
static final String NAME = "sc";
private ScoreExtractor() {}
@Override
public void writeTo(StreamOutput out) throws IOException {
// Nothing to write
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
public Object get(SearchHit hit) {
return hit.getScore();
}
@Override
public String innerHitName() {
return null;
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
return true;
}
@Override
public int hashCode() {
return 31;
}
@Override
public String toString() {
return "SCORE";
}
}

View File

@ -13,6 +13,10 @@ import java.io.IOException;
import java.util.Map; import java.util.Map;
public class SourceExtractor implements HitExtractor { public class SourceExtractor implements HitExtractor {
/**
* Stands for {@code _source}. We try to use short names for {@link HitExtractor}s
* to save a few bytes when when we send them back to the user.
*/
public static final String NAME = "s"; public static final String NAME = "s";
private final String fieldName; private final String fieldName;
@ -68,4 +72,4 @@ public class SourceExtractor implements HitExtractor {
* me of a hash table lookup. */ * me of a hash table lookup. */
return "#" + fieldName; return "#" + fieldName;
} }
} }

View File

@ -12,6 +12,11 @@ import java.util.Objects;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
/**
* {@link Expression}s that can be converted into Elasticsearch
* sorts, aggregations, or queries. They can also be extracted
* from the result of a search.
*/
public abstract class Attribute extends NamedExpression { public abstract class Attribute extends NamedExpression {
// empty - such as a top level attribute in SELECT cause // empty - such as a top level attribute in SELECT cause
@ -99,7 +104,7 @@ public abstract class Attribute extends NamedExpression {
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (super.equals(obj)) { if (super.equals(obj)) {
Attribute other = (Attribute) obj; Attribute other = (Attribute) obj;
return Objects.equals(qualifier, other.qualifier) return Objects.equals(qualifier, other.qualifier)
&& Objects.equals(nullable, other.nullable); && Objects.equals(nullable, other.nullable);
} }
@ -112,4 +117,4 @@ public abstract class Attribute extends NamedExpression {
} }
protected abstract String label(); protected abstract String label();
} }

View File

@ -19,7 +19,7 @@ public abstract class NamedExpression extends Expression {
public NamedExpression(Location location, String name, List<Expression> children, ExpressionId id) { public NamedExpression(Location location, String name, List<Expression> children, ExpressionId id) {
this(location, name, children, id, false); this(location, name, children, id, false);
} }
public NamedExpression(Location location, String name, List<Expression> children, ExpressionId id, boolean synthetic) { public NamedExpression(Location location, String name, List<Expression> children, ExpressionId id, boolean synthetic) {
super(location, children); super(location, children);
this.name = name; this.name = name;
@ -57,7 +57,7 @@ public abstract class NamedExpression extends Expression {
NamedExpression other = (NamedExpression) obj; NamedExpression other = (NamedExpression) obj;
return Objects.equals(synthetic, other.synthetic) return Objects.equals(synthetic, other.synthetic)
&& Objects.equals(id, other.id) && Objects.equals(id, other.id)
&& Objects.equals(name(), other.name()) && Objects.equals(name(), other.name())
&& Objects.equals(children(), other.children()); && Objects.equals(children(), other.children());
} }

View File

@ -5,6 +5,7 @@
*/ */
package org.elasticsearch.xpack.sql.expression.function; package org.elasticsearch.xpack.sql.expression.function;
import org.elasticsearch.xpack.sql.expression.function.Score;
import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction;
import org.elasticsearch.xpack.sql.expression.function.aggregate.Avg; import org.elasticsearch.xpack.sql.expression.function.aggregate.Avg;
import org.elasticsearch.xpack.sql.expression.function.aggregate.Correlation; import org.elasticsearch.xpack.sql.expression.function.aggregate.Correlation;
@ -56,18 +57,76 @@ import org.elasticsearch.xpack.sql.expression.function.scalar.math.Sin;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.Sinh; import org.elasticsearch.xpack.sql.expression.function.scalar.math.Sinh;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.Sqrt; import org.elasticsearch.xpack.sql.expression.function.scalar.math.Sqrt;
import org.elasticsearch.xpack.sql.expression.function.scalar.math.Tan; import org.elasticsearch.xpack.sql.expression.function.scalar.math.Tan;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import static java.util.Collections.unmodifiableMap; import static java.util.Collections.unmodifiableMap;
import static org.elasticsearch.xpack.sql.util.CollectionUtils.combine; import static java.util.Collections.unmodifiableList;
public class DefaultFunctionRegistry extends AbstractFunctionRegistry { public class DefaultFunctionRegistry extends AbstractFunctionRegistry {
private static final Collection<Class<? extends Function>> FUNCTIONS = combine(agg(), scalar()); private static final Collection<Class<? extends Function>> FUNCTIONS = unmodifiableList(Arrays.asList(
// Aggregate functions
Avg.class,
Count.class,
Max.class,
Min.class,
Sum.class,
// Statistics
Mean.class,
StddevPop.class,
VarPop.class,
Percentile.class,
PercentileRank.class,
SumOfSquares.class,
// Matrix aggs
MatrixCount.class,
MatrixMean.class,
MatrixVariance.class,
Skewness.class,
Kurtosis.class,
Covariance.class,
Correlation.class,
// Scalar functions
// Date
DayOfMonth.class,
DayOfWeek.class,
DayOfYear.class,
HourOfDay.class,
MinuteOfDay.class,
MinuteOfHour.class,
SecondOfMinute.class,
MonthOfYear.class,
Year.class,
// Math
Abs.class,
ACos.class,
ASin.class,
ATan.class,
Cbrt.class,
Ceil.class,
Cos.class,
Cosh.class,
Degrees.class,
E.class,
Exp.class,
Expm1.class,
Floor.class,
Log.class,
Log10.class,
Pi.class,
Radians.class,
Round.class,
Sin.class,
Sinh.class,
Sqrt.class,
Tan.class,
// Special
Score.class));
private static final Map<String, String> ALIASES; private static final Map<String, String> ALIASES;
static { static {
@ -92,75 +151,4 @@ public class DefaultFunctionRegistry extends AbstractFunctionRegistry {
protected Map<String, String> aliases() { protected Map<String, String> aliases() {
return ALIASES; return ALIASES;
} }
private static Collection<Class<? extends AggregateFunction>> agg() {
return Arrays.asList(
Avg.class,
Count.class,
Max.class,
Min.class,
Sum.class,
// statistics
Mean.class,
StddevPop.class,
VarPop.class,
Percentile.class,
PercentileRank.class,
SumOfSquares.class,
// Matrix aggs
MatrixCount.class,
MatrixMean.class,
MatrixVariance.class,
Skewness.class,
Kurtosis.class,
Covariance.class,
Correlation.class
);
}
private static Collection<Class<? extends ScalarFunction>> scalar() {
return combine(dateTimeFunctions(),
mathFunctions());
}
private static Collection<Class<? extends ScalarFunction>> dateTimeFunctions() {
return Arrays.asList(
DayOfMonth.class,
DayOfWeek.class,
DayOfYear.class,
HourOfDay.class,
MinuteOfDay.class,
MinuteOfHour.class,
SecondOfMinute.class,
MonthOfYear.class,
Year.class
);
}
private static Collection<Class<? extends ScalarFunction>> mathFunctions() {
return Arrays.asList(
Abs.class,
ACos.class,
ASin.class,
ATan.class,
Cbrt.class,
Ceil.class,
Cos.class,
Cosh.class,
Degrees.class,
E.class,
Exp.class,
Expm1.class,
Floor.class,
Log.class,
Log10.class,
Pi.class,
Radians.class,
Round.class,
Sin.class,
Sinh.class,
Sqrt.class,
Tan.class
);
}
} }

View File

@ -71,4 +71,4 @@ public abstract class Function extends NamedExpression {
public boolean functionEquals(Function f) { public boolean functionEquals(Function f) {
return f != null && getClass() == f.getClass() && arguments().equals(f.arguments()); return f != null && getClass() == f.getClass() && arguments().equals(f.arguments());
} }
} }

View File

@ -8,14 +8,16 @@ package org.elasticsearch.xpack.sql.expression.function;
import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException;
import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction;
import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunction;
import org.elasticsearch.xpack.sql.expression.function.Score;
public enum FunctionType { public enum FunctionType {
AGGREGATE(AggregateFunction.class),
AGGREGATE (AggregateFunction.class), SCALAR(ScalarFunction.class),
SCALAR(ScalarFunction.class); SCORE(Score.class);
private final Class<? extends Function> baseClass; private final Class<? extends Function> baseClass;
FunctionType(Class<? extends Function> base) { FunctionType(Class<? extends Function> base) {
this.baseClass = base; this.baseClass = base;
} }

View File

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.expression.function;
import org.elasticsearch.xpack.sql.expression.Attribute;
import org.elasticsearch.xpack.sql.expression.function.Function;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypes;
import static java.util.Collections.emptyList;
/**
* Function referring to the {@code _score} in a search. Only available
* in the search context, and only at the "root" so it can't be combined
* with other function.
*/
public class Score extends Function {
public Score(Location location) {
super(location, emptyList());
}
@Override
public DataType dataType() {
return DataTypes.FLOAT;
}
@Override
public Attribute toAttribute() {
return new ScoreAttribute(location());
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.expression.function;
import org.elasticsearch.xpack.sql.expression.Attribute;
import org.elasticsearch.xpack.sql.expression.ExpressionId;
import org.elasticsearch.xpack.sql.expression.function.FunctionAttribute;
import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.type.DataType;
import org.elasticsearch.xpack.sql.type.DataTypes;
/**
* {@link Attribute} that represents Elasticsearch's {@code _score}.
*/
public class ScoreAttribute extends FunctionAttribute {
/**
* Constructor for normal use.
*/
ScoreAttribute(Location location) {
this(location, "SCORE()", DataTypes.FLOAT, null, false, null, false);
}
/**
* Constructor for {@link #clone()}
*/
private ScoreAttribute(Location location, String name, DataType dataType, String qualifier, boolean nullable, ExpressionId id,
boolean synthetic) {
super(location, name, dataType, qualifier, nullable, id, synthetic, "SCORE");
}
@Override
protected Attribute clone(Location location, String name, DataType dataType, String qualifier, boolean nullable,
ExpressionId id, boolean synthetic) {
return new ScoreAttribute(location, name, dataType, qualifier, nullable, id, synthetic);
}
@Override
protected String label() {
return "SCORE";
}
}

View File

@ -38,10 +38,10 @@ public abstract class ScalarFunction extends Function {
} }
@Override @Override
public ScalarFunctionAttribute toAttribute() { public final ScalarFunctionAttribute toAttribute() {
if (lazyAttribute == null) { if (lazyAttribute == null) {
lazyAttribute = new ScalarFunctionAttribute(location(), name(), dataType(), id(), functionId(), asScript(), orderBy(), lazyAttribute = new ScalarFunctionAttribute(location(), name(), dataType(), id(), functionId(), asScript(), orderBy(),
asProcessorDefinition()); asProcessorDefinition());
} }
return lazyAttribute; return lazyAttribute;
} }
@ -110,4 +110,4 @@ public abstract class ScalarFunction extends Function {
public Expression orderBy() { public Expression orderBy() {
return null; return null;
} }
} }

View File

@ -73,4 +73,4 @@ public class ScalarFunctionAttribute extends FunctionAttribute {
protected String label() { protected String label() {
return "s->" + functionId(); return "s->" + functionId();
} }
} }

View File

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition;
import org.elasticsearch.xpack.sql.execution.search.extractor.ScoreExtractor;
import org.elasticsearch.xpack.sql.expression.Expression;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.HitExtractorProcessor;
import static java.util.Collections.emptyList;
public class ScoreProcessorDefinition extends ProcessorDefinition {
public ScoreProcessorDefinition(Expression expression) {
super(expression, emptyList());
}
@Override
public boolean resolved() {
return true;
}
@Override
public Processor asProcessor() {
return new HitExtractorProcessor(ScoreExtractor.INSTANCE);
}
}

View File

@ -45,13 +45,13 @@ public class ParsingException extends ClientSqlException {
return super.getMessage(); return super.getMessage();
} }
@Override
public String getMessage() {
return format(Locale.ROOT, "line %s:%s: %s", getLineNumber(), getColumnNumber(), getErrorMessage());
}
@Override @Override
public RestStatus status() { public RestStatus status() {
return RestStatus.BAD_REQUEST; return RestStatus.BAD_REQUEST;
} }
@Override
public String getMessage() {
return format(Locale.ROOT, "line %s:%s: %s", getLineNumber(), getColumnNumber(), getErrorMessage());
}
} }

View File

@ -12,28 +12,22 @@ import org.elasticsearch.xpack.sql.util.StringUtils;
import java.util.Collection; import java.util.Collection;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static java.lang.String.format; import static java.lang.String.format;
public class PlanningException extends ClientSqlException { public class PlanningException extends ClientSqlException {
private final Optional<RestStatus> status;
public PlanningException(String message, Object... args) { public PlanningException(String message, Object... args) {
super(message, args); super(message, args);
status = Optional.empty();
}
public PlanningException(String message, RestStatus restStatus, Object... args) {
super(message, args);
status = Optional.of(restStatus);
} }
public PlanningException(Collection<Failure> sources) { public PlanningException(Collection<Failure> sources) {
super(extractMessage(sources)); super(extractMessage(sources));
status = Optional.empty(); }
@Override
public RestStatus status() {
return RestStatus.BAD_REQUEST;
} }
private static String extractMessage(Collection<Failure> failures) { private static String extractMessage(Collection<Failure> failures) {
@ -41,9 +35,4 @@ public class PlanningException extends ClientSqlException {
.map(f -> format(Locale.ROOT, "line %s:%s: %s", f.source().location().getLineNumber(), f.source().location().getColumnNumber(), f.message())) .map(f -> format(Locale.ROOT, "line %s:%s: %s", f.source().location().getLineNumber(), f.source().location().getColumnNumber(), f.message()))
.collect(Collectors.joining(StringUtils.NEW_LINE, "Found " + failures.size() + " problem(s)\n", StringUtils.EMPTY)); .collect(Collectors.joining(StringUtils.NEW_LINE, "Found " + failures.size() + " problem(s)\n", StringUtils.EMPTY));
} }
@Override
public RestStatus status() {
return status.orElse(super.status());
}
} }

View File

@ -16,6 +16,7 @@ import org.elasticsearch.xpack.sql.expression.NamedExpression;
import org.elasticsearch.xpack.sql.expression.Order; import org.elasticsearch.xpack.sql.expression.Order;
import org.elasticsearch.xpack.sql.expression.function.Function; import org.elasticsearch.xpack.sql.expression.function.Function;
import org.elasticsearch.xpack.sql.expression.function.Functions; import org.elasticsearch.xpack.sql.expression.function.Functions;
import org.elasticsearch.xpack.sql.expression.function.ScoreAttribute;
import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction; import org.elasticsearch.xpack.sql.expression.function.aggregate.AggregateFunction;
import org.elasticsearch.xpack.sql.expression.function.aggregate.CompoundNumericAggregate; import org.elasticsearch.xpack.sql.expression.function.aggregate.CompoundNumericAggregate;
import org.elasticsearch.xpack.sql.expression.function.aggregate.Count; import org.elasticsearch.xpack.sql.expression.function.aggregate.Count;
@ -46,6 +47,7 @@ import org.elasticsearch.xpack.sql.querydsl.agg.LeafAgg;
import org.elasticsearch.xpack.sql.querydsl.container.AttributeSort; import org.elasticsearch.xpack.sql.querydsl.container.AttributeSort;
import org.elasticsearch.xpack.sql.querydsl.container.ComputedRef; import org.elasticsearch.xpack.sql.querydsl.container.ComputedRef;
import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer; import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer;
import org.elasticsearch.xpack.sql.querydsl.container.ScoreSort;
import org.elasticsearch.xpack.sql.querydsl.container.ScriptSort; import org.elasticsearch.xpack.sql.querydsl.container.ScriptSort;
import org.elasticsearch.xpack.sql.querydsl.container.Sort.Direction; import org.elasticsearch.xpack.sql.querydsl.container.Sort.Direction;
import org.elasticsearch.xpack.sql.querydsl.container.TotalCountRef; import org.elasticsearch.xpack.sql.querydsl.container.TotalCountRef;
@ -355,13 +357,12 @@ class QueryFolder extends RuleExecutor<PhysicalPlan> {
//FIXME: what about inner key //FIXME: what about inner key
queryC = withAgg.v1().addAggColumn(withAgg.v2().context()); queryC = withAgg.v1().addAggColumn(withAgg.v2().context());
if (withAgg.v2().innerKey() != null) { if (withAgg.v2().innerKey() != null) {
throw new PlanningException("innerkey/matrix stats not handled (yet)", RestStatus.BAD_REQUEST); throw new PlanningException("innerkey/matrix stats not handled (yet)");
} }
} }
} }
} // not an Alias or Function means it's an Attribute so apply the same logic as above
// not an Alias or a Function, means it's an Attribute so apply the same logic as above } else {
else {
GroupingAgg matchingGroup = null; GroupingAgg matchingGroup = null;
if (groupingContext != null) { if (groupingContext != null) {
matchingGroup = groupingContext.groupFor(ne); matchingGroup = groupingContext.groupFor(ne);
@ -432,7 +433,6 @@ class QueryFolder extends RuleExecutor<PhysicalPlan> {
private static class FoldOrderBy extends FoldingRule<OrderExec> { private static class FoldOrderBy extends FoldingRule<OrderExec> {
@Override @Override
protected PhysicalPlan rule(OrderExec plan) { protected PhysicalPlan rule(OrderExec plan) {
if (plan.child() instanceof EsQueryExec) { if (plan.child() instanceof EsQueryExec) {
EsQueryExec exec = (EsQueryExec) plan.child(); EsQueryExec exec = (EsQueryExec) plan.child();
QueryContainer qContainer = exec.queryContainer(); QueryContainer qContainer = exec.queryContainer();
@ -464,23 +464,20 @@ class QueryFolder extends RuleExecutor<PhysicalPlan> {
ScalarFunctionAttribute sfa = (ScalarFunctionAttribute) attr; ScalarFunctionAttribute sfa = (ScalarFunctionAttribute) attr;
// is there an expression to order by? // is there an expression to order by?
if (sfa.orderBy() != null) { if (sfa.orderBy() != null) {
Expression ob = sfa.orderBy(); if (sfa.orderBy() instanceof NamedExpression) {
if (ob instanceof NamedExpression) { Attribute at = ((NamedExpression) sfa.orderBy()).toAttribute();
Attribute at = ((NamedExpression) ob).toAttribute();
at = qContainer.aliases().getOrDefault(at, at); at = qContainer.aliases().getOrDefault(at, at);
qContainer = qContainer.sort(new AttributeSort(at, direction)); qContainer = qContainer.sort(new AttributeSort(at, direction));
} } else if (!sfa.orderBy().foldable()) {
// ignore constant // ignore constant
else if (!ob.foldable()) { throw new PlanningException("does not know how to order by expression %s", sfa.orderBy());
throw new PlanningException("does not know how to order by expression %s", ob);
} }
} }
// nope, use scripted sorting // nope, use scripted sorting
else { qContainer = qContainer.sort(new ScriptSort(sfa.script(), direction));
qContainer = qContainer.sort(new ScriptSort(sfa.script(), direction)); } else if (attr instanceof ScoreAttribute) {
} qContainer = qContainer.sort(new ScoreSort(direction));
} } else {
else {
qContainer = qContainer.sort(new AttributeSort(attr, direction)); qContainer = qContainer.sort(new AttributeSort(attr, direction));
} }
} }

View File

@ -104,4 +104,4 @@ abstract class Verifier {
return failures; return failures;
} }
} }

View File

@ -15,10 +15,12 @@ import org.elasticsearch.xpack.sql.expression.Attribute;
import org.elasticsearch.xpack.sql.expression.LiteralAttribute; import org.elasticsearch.xpack.sql.expression.LiteralAttribute;
import org.elasticsearch.xpack.sql.expression.NestedFieldAttribute; import org.elasticsearch.xpack.sql.expression.NestedFieldAttribute;
import org.elasticsearch.xpack.sql.expression.RootFieldAttribute; import org.elasticsearch.xpack.sql.expression.RootFieldAttribute;
import org.elasticsearch.xpack.sql.expression.function.ScoreAttribute;
import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute; import org.elasticsearch.xpack.sql.expression.function.scalar.ScalarFunctionAttribute;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.AttributeInput; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.AttributeInput;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ProcessorDefinition;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ReferenceInput; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ReferenceInput;
import org.elasticsearch.xpack.sql.expression.function.scalar.processor.definition.ScoreProcessorDefinition;
import org.elasticsearch.xpack.sql.querydsl.agg.AggPath; import org.elasticsearch.xpack.sql.querydsl.agg.AggPath;
import org.elasticsearch.xpack.sql.querydsl.agg.Aggs; import org.elasticsearch.xpack.sql.querydsl.agg.Aggs;
import org.elasticsearch.xpack.sql.querydsl.agg.GroupingAgg; import org.elasticsearch.xpack.sql.querydsl.agg.GroupingAgg;
@ -287,6 +289,9 @@ public class QueryContainer {
if (attr instanceof LiteralAttribute) { if (attr instanceof LiteralAttribute) {
return new Tuple<>(this, new ComputedRef(((LiteralAttribute) attr).asProcessorDefinition())); return new Tuple<>(this, new ComputedRef(((LiteralAttribute) attr).asProcessorDefinition()));
} }
if (attr instanceof ScoreAttribute) {
return new Tuple<>(this, new ComputedRef(new ScoreProcessorDefinition(attr)));
}
throw new SqlIllegalArgumentException("Unknown output attribute %s", attr); throw new SqlIllegalArgumentException("Unknown output attribute %s", attr);
} }
@ -371,4 +376,4 @@ public class QueryContainer {
throw new RuntimeException("error rendering", e); throw new RuntimeException("error rendering", e);
} }
} }
} }

View File

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.querydsl.container;
import java.util.Objects;
public class ScoreSort extends Sort {
public ScoreSort(Direction direction) {
super(direction);
}
@Override
public int hashCode() {
return Objects.hash(direction());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ScriptSort other = (ScriptSort) obj;
return Objects.equals(direction(), other.direction());
}
}

View File

@ -13,15 +13,26 @@ import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; import org.elasticsearch.search.aggregations.AggregatorFactories.Builder;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.sql.expression.RootFieldAttribute;
import org.elasticsearch.xpack.sql.expression.function.Score;
import org.elasticsearch.xpack.sql.querydsl.agg.Aggs; import org.elasticsearch.xpack.sql.querydsl.agg.Aggs;
import org.elasticsearch.xpack.sql.querydsl.agg.AvgAgg;
import org.elasticsearch.xpack.sql.querydsl.agg.GroupByColumnAgg; import org.elasticsearch.xpack.sql.querydsl.agg.GroupByColumnAgg;
import org.elasticsearch.xpack.sql.querydsl.container.AttributeSort;
import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer; import org.elasticsearch.xpack.sql.querydsl.container.QueryContainer;
import org.elasticsearch.xpack.sql.querydsl.container.ScoreSort;
import org.elasticsearch.xpack.sql.querydsl.container.Sort.Direction;
import org.elasticsearch.xpack.sql.querydsl.query.MatchQuery; import org.elasticsearch.xpack.sql.querydsl.query.MatchQuery;
import org.elasticsearch.xpack.sql.tree.Location; import org.elasticsearch.xpack.sql.tree.Location;
import org.elasticsearch.xpack.sql.type.DataTypes;
import static java.util.Collections.singletonList;
import static org.elasticsearch.search.sort.SortBuilders.fieldSort;
import static org.elasticsearch.search.sort.SortBuilders.scoreSort;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public class SourceGeneratorTests extends ESTestCase { public class SourceGeneratorTests extends ESTestCase {
@ -61,4 +72,49 @@ public class SourceGeneratorTests extends ESTestCase {
TermsAggregationBuilder termsBuilder = (TermsAggregationBuilder) aggBuilder.getAggregatorFactories().get(0); TermsAggregationBuilder termsBuilder = (TermsAggregationBuilder) aggBuilder.getAggregatorFactories().get(0);
assertEquals(10, termsBuilder.size()); assertEquals(10, termsBuilder.size());
} }
public void testSortNoneSpecified() {
QueryContainer container = new QueryContainer();
SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10));
assertEquals(singletonList(fieldSort("_doc")), sourceBuilder.sorts());
}
public void testSelectScoreForcesTrackingScore() {
QueryContainer container = new QueryContainer()
.addColumn(new Score(new Location(1, 1)).toAttribute());
SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10));
assertTrue(sourceBuilder.trackScores());
}
public void testSortScoreSpecified() {
QueryContainer container = new QueryContainer()
.sort(new ScoreSort(Direction.DESC));
SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10));
assertEquals(singletonList(scoreSort()), sourceBuilder.sorts());
}
public void testSortFieldSpecified() {
QueryContainer container = new QueryContainer()
.sort(new AttributeSort(new RootFieldAttribute(new Location(1, 1), "test", DataTypes.KEYWORD), Direction.ASC));
SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10));
assertEquals(singletonList(fieldSort("test").order(SortOrder.ASC)), sourceBuilder.sorts());
container = new QueryContainer()
.sort(new AttributeSort(new RootFieldAttribute(new Location(1, 1), "test", DataTypes.KEYWORD), Direction.DESC));
sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10));
assertEquals(singletonList(fieldSort("test").order(SortOrder.DESC)), sourceBuilder.sorts());
}
public void testNoSort() {
SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(new QueryContainer(), null, randomIntBetween(1, 10));
assertEquals(singletonList(fieldSort("_doc").order(SortOrder.ASC)), sourceBuilder.sorts());
}
public void testNoSortIfAgg() {
QueryContainer container = new QueryContainer()
.addGroups(singletonList(new GroupByColumnAgg("group_id", "", "group_column")))
.addAgg("group_id", new AvgAgg("agg_id", "", "avg_column"));
SearchSourceBuilder sourceBuilder = SourceGenerator.sourceBuilder(container, null, randomIntBetween(1, 10));
assertNull(sourceBuilder.sorts());
}
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.execution.search.extractor;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.test.ESTestCase;
public class ScoreExtractorTests extends ESTestCase {
public void testGet() {
int times = between(1, 1000);
for (int i = 0; i < times; i++) {
float score = randomFloat();
SearchHit hit = new SearchHit(1);
hit.score(score);
assertEquals(score, ScoreExtractor.INSTANCE.get(hit));
}
}
public void testToString() {
assertEquals("SCORE", ScoreExtractor.INSTANCE.toString());
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.sql.parser;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.sql.expression.NamedExpression;
import org.elasticsearch.xpack.sql.expression.Order;
import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute;
import org.elasticsearch.xpack.sql.expression.UnresolvedStar;
import org.elasticsearch.xpack.sql.expression.function.UnresolvedFunction;
import org.elasticsearch.xpack.sql.parser.SqlParser;
import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan;
import org.elasticsearch.xpack.sql.plan.logical.OrderBy;
import org.elasticsearch.xpack.sql.plan.logical.Project;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static java.util.stream.Collectors.toList;
public class SqlParserTests extends ESTestCase {
public void testSelectStar() {
singleProjection(project(parseStatement("SELECT * FROM foo")), UnresolvedStar.class);
}
private <T> T singleProjection(Project project, Class<T> type) {
assertThat(project.projections(), hasSize(1));
NamedExpression p = project.projections().get(0);
assertThat(p, instanceOf(type));
return type.cast(p);
}
public void testSelectField() {
UnresolvedAttribute a = singleProjection(project(parseStatement("SELECT bar FROM foo")), UnresolvedAttribute.class);
assertEquals("bar", a.name());
}
public void testSelectScore() {
UnresolvedFunction f = singleProjection(project(parseStatement("SELECT SCORE() FROM foo")), UnresolvedFunction.class);
assertEquals("SCORE", f.functionName());
}
public void testOrderByField() {
Order.OrderDirection dir = randomFrom(Order.OrderDirection.values());
OrderBy ob = orderBy(parseStatement("SELECT * FROM foo ORDER BY bar" + stringForDirection(dir)));
assertThat(ob.order(), hasSize(1));
Order o = ob.order().get(0);
assertEquals(dir, o.direction());
assertThat(o.child(), instanceOf(UnresolvedAttribute.class));
UnresolvedAttribute a = (UnresolvedAttribute) o.child();
assertEquals("bar", a.name());
}
public void testOrderByScore() {
Order.OrderDirection dir = randomFrom(Order.OrderDirection.values());
OrderBy ob = orderBy(parseStatement("SELECT * FROM foo ORDER BY SCORE()" + stringForDirection(dir)));
assertThat(ob.order(), hasSize(1));
Order o = ob.order().get(0);
assertEquals(dir, o.direction());
assertThat(o.child(), instanceOf(UnresolvedFunction.class));
UnresolvedFunction f = (UnresolvedFunction) o.child();
assertEquals("SCORE", f.functionName());
}
public void testOrderByTwo() {
Order.OrderDirection dir0 = randomFrom(Order.OrderDirection.values());
Order.OrderDirection dir1 = randomFrom(Order.OrderDirection.values());
OrderBy ob = orderBy(parseStatement(
" SELECT *"
+ " FROM foo"
+ " ORDER BY bar" + stringForDirection(dir0) + ", baz" + stringForDirection(dir1)));
assertThat(ob.order(), hasSize(2));
Order o = ob.order().get(0);
assertEquals(dir0, o.direction());
assertThat(o.child(), instanceOf(UnresolvedAttribute.class));
UnresolvedAttribute a = (UnresolvedAttribute) o.child();
assertEquals("bar", a.name());
o = ob.order().get(1);
assertEquals(dir1, o.direction());
assertThat(o.child(), instanceOf(UnresolvedAttribute.class));
a = (UnresolvedAttribute) o.child();
assertEquals("baz", a.name());
}
private LogicalPlan parseStatement(String sql) {
return new SqlParser().createStatement(sql);
}
private Project project(LogicalPlan plan) {
List<Project> sync = new ArrayList<Project>(1);
projectRecur(plan, sync);
assertThat("expected only one SELECT", sync, hasSize(1));
return sync.get(0);
}
private void projectRecur(LogicalPlan plan, List<Project> sync) {
if (plan instanceof Project) {
sync.add((Project) plan);
return;
}
for (LogicalPlan child : plan.children()) {
projectRecur(child, sync);
}
}
/**
* Find the one and only {@code ORDER BY} in a plan.
*/
private OrderBy orderBy(LogicalPlan plan) {
List<LogicalPlan> l = plan.children().stream()
.filter(c -> c instanceof OrderBy)
.collect(toList());
assertThat("expected only one ORDER BY", l, hasSize(1));
return (OrderBy) l.get(0);
}
/**
* Convert a direction into a string that represents that parses to
* that direction.
*/
private String stringForDirection(Order.OrderDirection dir) {
String dirStr = dir.toString();
return randomBoolean() && dirStr.equals("ASC") ? "" : " " + dirStr;
}
}