Repository: calcite Updated Branches: refs/heads/master ce0514690 -> 79af1c9ba
http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java index 2ec4a0d..b046cb5 100644 --- a/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java +++ b/elasticsearch/src/main/java/org/apache/calcite/adapter/elasticsearch/QueryBuilders.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Objects; /** @@ -30,6 +29,10 @@ import java.util.Objects; * high-level client dependency on core modules (like lucene, netty, XContent etc.) which * is not compatible between different major versions. * + * <p>The goal of ES adapter is to + * be compatible with any elastic version or even to connect to clusters with different + * versions simultaneously. + * * <p>Jackson API is used to generate ES query as JSON document. */ class QueryBuilders { @@ -57,6 +60,16 @@ class QueryBuilders { } /** + * A Query that matches documents containing a single character term. + * + * @param name The name of the field + * @param value The value of the term + */ + static TermQueryBuilder termQuery(String name, char value) { + return new TermQueryBuilder(name, value); + } + + /** * A Query that matches documents containing a term. * * @param name The name of the field @@ -107,6 +120,16 @@ class QueryBuilders { } /** + * A filer for a field based on several terms matching on any of them. + * + * @param name The field name + * @param values The terms + */ + static TermsQueryBuilder termsQuery(String name, Iterable<?> values) { + return new TermsQueryBuilder(name, values); + } + + /** * A Query that matches documents within an range of terms. * * @param name The field name @@ -153,6 +176,13 @@ class QueryBuilders { } /** + * A query that matches on all documents. + */ + static MatchAllQueryBuilder matchAll() { + return new MatchAllQueryBuilder(); + } + + /** * Base class to build ES queries */ abstract static class QueryBuilder { @@ -246,34 +276,49 @@ class QueryBuilders { generator.writeFieldName("term"); generator.writeStartObject(); generator.writeFieldName(fieldName); - writeScalar(generator, value); + writeObject(generator, value); + generator.writeEndObject(); + generator.writeEndObject(); + } + } + + /** + * A filter for a field based on several terms matching on any of them. + */ + private static class TermsQueryBuilder extends QueryBuilder { + private final String fieldName; + private final Iterable<?> values; + + private TermsQueryBuilder(final String fieldName, final Iterable<?> values) { + this.fieldName = Objects.requireNonNull(fieldName, "fieldName"); + this.values = Objects.requireNonNull(values, "values"); + } + + @Override void writeJson(final JsonGenerator generator) throws IOException { + generator.writeStartObject(); + generator.writeFieldName("terms"); + generator.writeStartObject(); + generator.writeFieldName(fieldName); + generator.writeStartArray(); + for (Object value: values) { + writeObject(generator, value); + } + generator.writeEndArray(); generator.writeEndObject(); generator.writeEndObject(); } } /** - * Write single simple (scalar) value (string, number, boolean or null) to json output. + * Write usually simple (scalar) value (string, number, boolean or null) to json output. + * In case of complex objects delegates to jackson serialization. * * @param generator api to generate JSON document * @param value JSON value to write * @throws IOException if can't write to output */ - private static void writeScalar(JsonGenerator generator, Object value) throws IOException { - if (value == null) { - generator.writeNull(); - } else if (value instanceof CharSequence) { - generator.writeString(Objects.toString(value)); - } else if (value instanceof Number) { - // write numbers as string - generator.writeNumber(value.toString()); - } else if (value instanceof Boolean) { - generator.writeBoolean((Boolean) value); - } else { - final String message = String.format(Locale.ROOT, "Unsupported type %s : %s", - value.getClass(), value); - throw new IllegalArgumentException(message); - } + private static void writeObject(JsonGenerator generator, Object value) throws IOException { + generator.writeObject(value); } /** @@ -340,13 +385,13 @@ class QueryBuilders { if (gt != null) { final String op = gte ? "gte" : "gt"; generator.writeFieldName(op); - writeScalar(generator, gt); + writeObject(generator, gt); } if (lt != null) { final String op = lte ? "lte" : "lt"; generator.writeFieldName(op); - writeScalar(generator, lt); + writeObject(generator, lt); } if (format != null) { @@ -418,6 +463,27 @@ class QueryBuilders { generator.writeEndObject(); } } + + /** + * A query that matches on all documents. + * <pre> + * { + * "match_all": {} + * } + * </pre> + */ + static class MatchAllQueryBuilder extends QueryBuilder { + + private MatchAllQueryBuilder() {} + + @Override void writeJson(final JsonGenerator generator) throws IOException { + generator.writeStartObject(); + generator.writeFieldName("match_all"); + generator.writeStartObject(); + generator.writeEndObject(); + generator.writeEndObject(); + } + } } // End QueryBuilders.java http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/AggregationTest.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/AggregationTest.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/AggregationTest.java new file mode 100644 index 0000000..8115bc7 --- /dev/null +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/AggregationTest.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.elasticsearch; + +import org.apache.calcite.jdbc.CalciteConnection; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.schema.impl.ViewTable; +import org.apache.calcite.schema.impl.ViewTableMacro; +import org.apache.calcite.test.CalciteAssert; +import org.apache.calcite.test.ElasticsearchChecker; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableMap; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Testing Elastic Search aggregation transformations. + */ +public class AggregationTest { + + @ClassRule + public static final EmbeddedElasticsearchPolicy NODE = EmbeddedElasticsearchPolicy.create(); + + private static final String NAME = "aggs"; + + @BeforeClass + public static void setupInstance() throws Exception { + + final Map<String, String> mappings = ImmutableMap.of("cat1", "keyword", + "cat2", "keyword", "cat3", "keyword", + "val1", "long", "val2", "long"); + + NODE.createIndex(NAME, mappings); + + String doc1 = "{'cat1': 'a', 'cat2': 'g', 'val1': 1 }".replace('\'', '"'); + String doc2 = "{'cat2': 'g', 'cat3': 'y', 'val2': 5 }".replace('\'', '"'); + String doc3 = "{'cat1': 'b', 'cat2':'h', 'cat3': 'z', 'val1': 7, 'val2': '42'}" + .replace('\'', '"'); + + List<ObjectNode> docs = new ArrayList<>(); + for (String text: Arrays.asList(doc1, doc2, doc3)) { + docs.add((ObjectNode) NODE.mapper().readTree(text)); + } + + NODE.insertBulk(NAME, docs); + } + + private CalciteAssert.ConnectionFactory newConnectionFactory() { + return new CalciteAssert.ConnectionFactory() { + @Override public Connection createConnection() throws SQLException { + final Connection connection = DriverManager.getConnection("jdbc:calcite:lex=JAVA"); + final SchemaPlus root = connection.unwrap(CalciteConnection.class).getRootSchema(); + + root.add("elastic", new ElasticsearchSchema(NODE.restClient(), NODE.mapper(), NAME)); + + // add calcite view programmatically + final String viewSql = String.format(Locale.ROOT, + "select _MAP['cat1'] AS \"cat1\", " + + " _MAP['cat2'] AS \"cat2\", " + + " _MAP['cat3'] AS \"cat3\", " + + " _MAP['val1'] AS \"val1\", " + + " _MAP['val2'] AS \"val2\" " + + " from \"elastic\".\"%s\"", NAME); + + ViewTableMacro macro = ViewTable.viewMacro(root, viewSql, + Collections.singletonList("elastic"), Arrays.asList("elastic", "view"), false); + root.add("view", macro); + return connection; + } + }; + } + + @Test + public void countStar() { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select count(*) from view") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("_source:false, size:0")) + .returns("EXPR$0=3\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select count(*) from view where cat1 = 'a'") + .returns("EXPR$0=1\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select count(*) from view where cat1 in ('a', 'b')") + .returns("EXPR$0=2\n"); + } + + @Test + public void all() throws Exception { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select count(*), sum(val1), sum(val2) from view") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("_source:false, size:0", + "aggregations:{'EXPR$0.value_count.field': '_id'", + "'EXPR$1.sum.field': 'val1'", + "'EXPR$2.sum.field': 'val2'}")) + .returns("EXPR$0=3; EXPR$1=8.0; EXPR$2=47.0\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select min(val1), max(val2), count(*) from view") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("_source:false, size:0", + "aggregations:{'EXPR$0.min.field': 'val1'", + "'EXPR$1.max.field': 'val2'", + "'EXPR$2.value_count.field': '_id'}")) + .returns("EXPR$0=1.0; EXPR$1=42.0; EXPR$2=3\n"); + } + + @Test + public void cat1() throws Exception { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, sum(val1), sum(val2) from view group by cat1") + .returnsUnordered("cat1=null; EXPR$1=0.0; EXPR$2=5.0", + "cat1=a; EXPR$1=1.0; EXPR$2=0.0", + "cat1=b; EXPR$1=7.0; EXPR$2=42.0"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, count(*) from view group by cat1") + .returnsUnordered("cat1=null; EXPR$1=1", + "cat1=a; EXPR$1=1", + "cat1=b; EXPR$1=1"); + + // different order for agg functions + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select count(*), cat1 from view group by cat1") + .returnsUnordered("EXPR$0=1; cat1=a", + "EXPR$0=1; cat1=b", + "EXPR$0=1; cat1=null"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, count(*), sum(val1), sum(val2) from view group by cat1") + .returnsUnordered("cat1=a; EXPR$1=1; EXPR$2=1.0; EXPR$3=0.0", + "cat1=b; EXPR$1=1; EXPR$2=7.0; EXPR$3=42.0", + "cat1=null; EXPR$1=1; EXPR$2=0.0; EXPR$3=5.0"); + } + + @Test + public void cat2() throws Exception { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat2, min(val1), max(val1), min(val2), max(val2) from view group by cat2") + .returnsUnordered("cat2=g; EXPR$1=1.0; EXPR$2=1.0; EXPR$3=5.0; EXPR$4=5.0", + "cat2=h; EXPR$1=7.0; EXPR$2=7.0; EXPR$3=42.0; EXPR$4=42.0"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat2, sum(val1), sum(val2) from view group by cat2") + .returnsUnordered("cat2=g; EXPR$1=1.0; EXPR$2=5.0", + "cat2=h; EXPR$1=7.0; EXPR$2=42.0"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat2, count(*) from view group by cat2") + .returnsUnordered("cat2=g; EXPR$1=2", + "cat2=h; EXPR$1=1"); + } + + @Test + public void cat1Cat2() throws Exception { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, cat2, sum(val1), sum(val2) from view group by cat1, cat2") + .returnsUnordered("cat1=a; cat2=g; EXPR$2=1.0; EXPR$3=0.0", + "cat1=null; cat2=g; EXPR$2=0.0; EXPR$3=5.0", + "cat1=b; cat2=h; EXPR$2=7.0; EXPR$3=42.0"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, cat2, count(*) from view group by cat1, cat2") + .returnsUnordered("cat1=a; cat2=g; EXPR$2=1", + "cat1=null; cat2=g; EXPR$2=1", + "cat1=b; cat2=h; EXPR$2=1"); + } + + @Test + public void cat1Cat3() throws Exception { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, cat3, sum(val1), sum(val2) from view group by cat1, cat3") + .returnsUnordered("cat1=a; cat3=null; EXPR$2=1.0; EXPR$3=0.0", + "cat1=null; cat3=y; EXPR$2=0.0; EXPR$3=5.0", + "cat1=b; cat3=z; EXPR$2=7.0; EXPR$3=42.0"); + } + + @Test + public void cat1Cat2Cat3() throws Exception { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select cat1, cat2, cat3, count(*), sum(val1), sum(val2) from view " + + "group by cat1, cat2, cat3") + .returnsUnordered("cat1=a; cat2=g; cat3=null; EXPR$3=1; EXPR$4=1.0; EXPR$5=0.0", + "cat1=b; cat2=h; cat3=z; EXPR$3=1; EXPR$4=7.0; EXPR$5=42.0", + "cat1=null; cat2=g; cat3=y; EXPR$3=1; EXPR$4=0.0; EXPR$5=5.0"); + } +} + +// End AggregationTest.java http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/BooleanLogicTest.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/BooleanLogicTest.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/BooleanLogicTest.java index c3b416c..c461905 100644 --- a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/BooleanLogicTest.java +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/BooleanLogicTest.java @@ -128,6 +128,7 @@ public class BooleanLogicTest { assertSingle("select * from view where num > 41"); assertSingle("select * from view where num > 0"); assertSingle("select * from view where (a = 'a' and b = 'b') or (num = 42 and c = 'c')"); + assertSingle("select * from view where c = 'c' and (a in ('a', 'b') or num in (41, 42))"); assertSingle("select * from view where (a = 'a' or b = 'b') or (num = 42 and c = 'c')"); assertSingle("select * from view where a = 'a' and (b = '0' or (b = 'b' and " + "(c = '0' or (c = 'c' and num = 42))))"); http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java index d523bba..3d21b03 100644 --- a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticSearchAdapterTest.java @@ -92,7 +92,7 @@ public class ElasticSearchAdapterTest { private CalciteAssert.ConnectionFactory newConnectionFactory() { return new CalciteAssert.ConnectionFactory() { @Override public Connection createConnection() throws SQLException { - final Connection connection = DriverManager.getConnection("jdbc:calcite:"); + final Connection connection = DriverManager.getConnection("jdbc:calcite:lex=JAVA"); final SchemaPlus root = connection.unwrap(CalciteConnection.class).getRootSchema(); root.add("elastic", new ElasticsearchSchema(NODE.restClient(), NODE.mapper(), ZIPS)); @@ -108,7 +108,7 @@ public class ElasticSearchAdapterTest { ViewTableMacro macro = ViewTable.viewMacro(root, viewSql, Collections.singletonList("elastic"), Arrays.asList("elastic", "view"), false); - root.add("ZIPS", macro); + root.add("zips", macro); return connection; } @@ -126,7 +126,7 @@ public class ElasticSearchAdapterTest { @Test public void view() { calciteAssert() - .query("select * from zips where \"city\" = 'BROOKLYN'") + .query("select * from zips where city = 'BROOKLYN'") .returns("city=BROOKLYN; longitude=-73.956985; latitude=40.646694; " + "pop=111396; state=NY; id=11226\n") .returnsCount(1); @@ -141,42 +141,53 @@ public class ElasticSearchAdapterTest { CalciteAssert.that() .with(newConnectionFactory()) - .query("select * from \"elastic\".\"zips\" where _MAP['Foo'] = '_MISSING_'") + .query("select * from elastic.zips where _MAP['Foo'] = '_MISSING_'") .returnsCount(0); } @Test - public void basic() throws Exception { + public void basic() { CalciteAssert.that() .with(newConnectionFactory()) - .query("select * from \"elastic\".\"zips\" where _MAP['city'] = 'BROOKLYN'") + // by default elastic returns max 10 records + .query("select * from elastic.zips") + .runs(); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select * from elastic.zips where _MAP['city'] = 'BROOKLYN'") .returnsCount(1); CalciteAssert.that() .with(newConnectionFactory()) - .query("select * from \"elastic\".\"zips\" where" + .query("select * from elastic.zips where" + " _MAP['city'] in ('BROOKLYN', 'WASHINGTON')") .returnsCount(2); // lower-case CalciteAssert.that() .with(newConnectionFactory()) - .query("select * from \"elastic\".\"zips\" where " + .query("select * from elastic.zips where " + "_MAP['city'] in ('brooklyn', 'Brooklyn', 'BROOK') ") .returnsCount(0); // missing field CalciteAssert.that() .with(newConnectionFactory()) - .query("select * from \"elastic\".\"zips\" where _MAP['CITY'] = 'BROOKLYN'") + .query("select * from elastic.zips where _MAP['CITY'] = 'BROOKLYN'") .returnsCount(0); // limit works CalciteAssert.that() .with(newConnectionFactory()) - .query("select * from \"elastic\".\"zips\" limit 42") + .query("select * from elastic.zips limit 42") .returnsCount(42); + // limit 0 + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select * from elastic.zips limit 0") + .returnsCount(0); } @Test public void testSort() { @@ -186,14 +197,14 @@ public class ElasticSearchAdapterTest { + " ElasticsearchTableScan(table=[[elastic, zips]])"; calciteAssert() - .query("select * from zips order by \"state\"") + .query("select * from zips order by state") .returnsCount(10) .explainContains(explain); } @Test public void testSortLimit() { - final String sql = "select \"state\", \"pop\" from zips\n" - + "order by \"state\", \"pop\" offset 2 rows fetch next 3 rows only"; + final String sql = "select state, pop from zips\n" + + "order by state, pop offset 2 rows fetch next 3 rows only"; calciteAssert() .query(sql) .returnsUnordered("state=AK; pop=32383", @@ -201,44 +212,75 @@ public class ElasticSearchAdapterTest { "state=AL; pop=43862") .queryContains( ElasticsearchChecker.elasticsearchChecker( - "\"_source\" : [\"state\", \"pop\"]", - "\"sort\": [ {\"state\": \"asc\"}, {\"pop\": \"asc\"}]", - "\"from\": 2", - "\"size\": 3")); + "'_source' : ['state', 'pop']", + "sort: [ {state: 'asc'}, {pop: 'asc'}]", + "from: 2", + "size: 3")); } - + /** + * Sort by multiple fields (in different direction: asc/desc) + */ + @Test public void sortAscDesc() { + final String sql = "select city, state, pop from zips\n" + + "order by pop desc, state asc, city desc limit 3"; + calciteAssert() + .query(sql) + .returnsOrdered("city=CHICAGO; state=IL; pop=112047", + "city=BROOKLYN; state=NY; pop=111396", + "city=NEW YORK; state=NY; pop=106564") + .queryContains( + ElasticsearchChecker.elasticsearchChecker( + "'_source':['city','state','pop']", + "sort:[{pop:'desc'}, {state:'asc'}, {city:'desc'}]", + "size:3")); + } @Test public void testOffsetLimit() { - final String sql = "select \"state\", \"id\" from zips\n" + final String sql = "select state, id from zips\n" + "offset 2 fetch next 3 rows only"; calciteAssert() .query(sql) .runs() + .returnsCount(3) .queryContains( ElasticsearchChecker.elasticsearchChecker( - "\"from\": 2", - "\"size\": 3", - "\"_source\" : [\"state\", \"id\"]")); + "_source : ['state', 'id']", + "from: 2", + "size: 3")); } @Test public void testLimit() { - final String sql = "select \"state\", \"id\" from zips\n" + final String sql = "select state, id from zips\n" + "fetch next 3 rows only"; calciteAssert() .query(sql) .runs() + .returnsCount(3) .queryContains( ElasticsearchChecker.elasticsearchChecker( - "\"size\": 3", - "\"_source\" : [\"state\", \"id\"]")); + "'_source':['state','id']", + "size:3")); + } + + @Test + public void limit2() { + final String sql = "select id from zips limit 5"; + calciteAssert() + .query(sql) + .runs() + .returnsCount(5) + .queryContains( + ElasticsearchChecker.elasticsearchChecker( + "'_source':['id']", + "size:5")); } @Test public void testFilterSort() { final String sql = "select * from zips\n" - + "where \"state\" = 'CA' and \"pop\" >= 94000\n" - + "order by \"state\", \"pop\""; + + "where state = 'CA' and pop >= 94000\n" + + "order by state, pop"; final String explain = "PLAN=ElasticsearchToEnumerableConverter\n" + " ElasticsearchSort(sort0=[$4], sort1=[$3], dir0=[ASC], dir1=[ASC])\n" + " ElasticsearchProject(city=[CAST(ITEM($0, 'city')):VARCHAR(20) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\"], longitude=[CAST(ITEM(ITEM($0, 'loc'), 0)):FLOAT], latitude=[CAST(ITEM(ITEM($0, 'loc'), 1)):FLOAT], pop=[CAST(ITEM($0, 'pop')):INTEGER], state=[CAST(ITEM($0, 'state')):VARCHAR(2) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\"], id=[CAST(ITEM($0, 'id')):VARCHAR(5) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\"])\n" @@ -253,24 +295,24 @@ public class ElasticSearchAdapterTest { "city=BELL GARDENS; longitude=-118.17205; latitude=33.969177;" + " pop=99568; state=CA; id=90201") .queryContains( - ElasticsearchChecker.elasticsearchChecker("\"query\" : " - + "{\"constant_score\":{\"filter\":{\"bool\":" - + "{\"must\":[{\"term\":{\"state\":\"CA\"}}," - + "{\"range\":{\"pop\":{\"gte\":94000}}}]}}}}", - "\"script_fields\": {\"longitude\":{\"script\":\"params._source.loc[0]\"}, " - + "\"latitude\":{\"script\":\"params._source.loc[1]\"}, " - + "\"city\":{\"script\": \"params._source.city\"}, " - + "\"pop\":{\"script\": \"params._source.pop\"}, " - + "\"state\":{\"script\": \"params._source.state\"}, " - + "\"id\":{\"script\": \"params._source.id\"}}", - "\"sort\": [ {\"state\": \"asc\"}, {\"pop\": \"asc\"}]")) + ElasticsearchChecker.elasticsearchChecker("'query' : " + + "{'constant_score':{filter:{bool:" + + "{must:[{term:{state:'CA'}}," + + "{range:{pop:{gte:94000}}}]}}}}", + "'script_fields': {longitude:{script:'params._source.loc[0]'}, " + + "latitude:{script:'params._source.loc[1]'}, " + + "city:{script: 'params._source.city'}, " + + "pop:{script: 'params._source.pop'}, " + + "state:{script: 'params._source.state'}, " + + "id:{script: 'params._source.id'}}", + "sort: [ {state: 'asc'}, {pop: 'asc'}]")) .explainContains(explain); } @Test public void testFilterSortDesc() { final String sql = "select * from zips\n" - + "where \"pop\" BETWEEN 95000 AND 100000\n" - + "order by \"state\" desc, \"pop\""; + + "where pop BETWEEN 95000 AND 100000\n" + + "order by state desc, pop"; calciteAssert() .query(sql) .limit(4) @@ -279,41 +321,20 @@ public class ElasticSearchAdapterTest { "city=BELL GARDENS; longitude=-118.17205; latitude=33.969177; pop=99568; state=CA; id=90201"); } - @Ignore("Known issue when predicate analyzer doesn't simplify the expression (a = 1 and a > 0) ") - @Test public void testFilterRedundant() { - // known issue when PredicateAnalyzer doesn't simplify expressions - // (a < 3 and and a > 0 and a = 1) equivalent to (a = 1) - final String sql = "select * from zips\n" - + "where \"state\" > 'CA' and \"state\" < 'AZ' and \"state\" = 'OK'"; - calciteAssert() - .query(sql) - .runs() - .queryContains( - ElasticsearchChecker.elasticsearchChecker("" - + "\"query\" : {\"constant_score\":{\"filter\":{\"bool\":" - + "{\"must\":[{\"term\":{\"state\":\"OK\"}}]}}}}", - "\"script_fields\": {\"longitude\":{\"script\":\"params._source.loc[0]\"}, " - + "\"latitude\":{\"script\":\"params._source.loc[1]\"}, " - + "\"city\":{\"script\": \"params._source.city\"}, " - + "\"pop\":{\"script\": \"params._source.pop\"}, \"state\":{\"script\": \"params._source.state\"}, " - + "\"id\":{\"script\": \"params._source.id\"}}" - )); - } - @Test public void testInPlan() { final String[] searches = { - "\"query\" : {\"constant_score\":{\"filter\":{\"bool\":{\"should\":" - + "[{\"term\":{\"pop\":96074}},{\"term\":{\"pop\":99568}}]}}}}", - "\"script_fields\": {\"longitude\":{\"script\":\"params._source.loc[0]\"}, " - + "\"latitude\":{\"script\":\"params._source.loc[1]\"}, " - + "\"city\":{\"script\": \"params._source.city\"}, " - + "\"pop\":{\"script\": \"params._source.pop\"}, " - + "\"state\":{\"script\": \"params._source.state\"}, " - + "\"id\":{\"script\": \"params._source.id\"}}" + "'query' : {'constant_score':{filter:{bool:{should:" + + "[{term:{pop:96074}},{term:{pop:99568}}]}}}}", + "'script_fields': {longitude:{script:'params._source.loc[0]'}, " + + "latitude:{script:'params._source.loc[1]'}, " + + "city:{script: 'params._source.city'}, " + + "pop:{script: 'params._source.pop'}, " + + "state:{script: 'params._source.state'}, " + + "id:{script: 'params._source.id'}}" }; calciteAssert() - .query("select * from zips where \"pop\" in (96074, 99568)") + .query("select * from zips where pop in (96074, 99568)") .returnsUnordered( "city=BELL GARDENS; longitude=-118.17205; latitude=33.969177; pop=99568; state=CA; id=90201", "city=LOS ANGELES; longitude=-118.258189; latitude=34.007856; pop=96074; state=CA; id=90011" @@ -323,14 +344,14 @@ public class ElasticSearchAdapterTest { @Test public void testZips() { calciteAssert() - .query("select \"state\", \"city\" from zips") + .query("select state, city from zips") .returnsCount(10); } @Test public void testProject() { - final String sql = "select \"state\", \"city\", 0 as \"zero\"\n" + final String sql = "select state, city, 0 as zero\n" + "from zips\n" - + "order by \"state\", \"city\""; + + "order by state, city"; calciteAssert() .query(sql) @@ -338,11 +359,11 @@ public class ElasticSearchAdapterTest { .returnsUnordered("state=AK; city=ANCHORAGE; zero=0", "state=AK; city=FAIRBANKS; zero=0") .queryContains( - ElasticsearchChecker.elasticsearchChecker("\"script_fields\": " - + "{\"zero\":{\"script\": \"0\"}, " - + "\"state\":{\"script\": \"params._source.state\"}, " - + "\"city\":{\"script\": \"params._source.city\"}}", - "\"sort\": [ {\"state\": \"asc\"}, {\"city\": \"asc\"}]")); + ElasticsearchChecker.elasticsearchChecker("script_fields:" + + "{zero:{script:'0'}," + + "state:{script:'params._source.state'}," + + "city:{script:'params._source.city'}}", + "sort:[{state:'asc'},{city:'asc'}]")); } @Test public void testFilter() { @@ -350,8 +371,9 @@ public class ElasticSearchAdapterTest { + " ElasticsearchProject(state=[CAST(ITEM($0, 'state')):VARCHAR(2) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\"], city=[CAST(ITEM($0, 'city')):VARCHAR(20) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\"])\n" + " ElasticsearchFilter(condition=[=(CAST(ITEM($0, 'state')):VARCHAR(2) CHARACTER SET \"ISO-8859-1\" COLLATE \"ISO-8859-1$en_US$primary\", 'CA')])\n" + " ElasticsearchTableScan(table=[[elastic, zips]])"; + calciteAssert() - .query("select \"state\", \"city\" from zips where \"state\" = 'CA'") + .query("select state, city from zips where state = 'CA'") .limit(3) .returnsUnordered("state=CA; city=BELL GARDENS", "state=CA; city=LOS ANGELES", @@ -361,17 +383,142 @@ public class ElasticSearchAdapterTest { @Test public void testFilterReversed() { calciteAssert() - .query("select \"state\", \"city\" from zips where 'WI' < \"state\" order by \"city\"") + .query("select state, city from zips where 'WI' < state order by city") .limit(2) .returnsUnordered("state=WV; city=BECKLEY", "state=WY; city=CHEYENNE"); calciteAssert() - .query("select \"state\", \"city\" from zips where \"state\" > 'WI' order by \"city\"") + .query("select state, city from zips where state > 'WI' order by city") .limit(2) .returnsUnordered("state=WV; city=BECKLEY", "state=WY; city=CHEYENNE"); } + @Test + public void agg1() { + calciteAssert() + .query("select count(*) from zips") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0")) + .returns("EXPR$0=149\n"); + + // check with limit (should still return correct result). + calciteAssert() + .query("select count(*) from zips limit 1") + .returns("EXPR$0=149\n"); + + calciteAssert() + .query("select count(*) as cnt from zips") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0")) + .returns("cnt=149\n"); + + calciteAssert() + .query("select min(pop), max(pop) from zips") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0", + "aggregations:{'EXPR$0':{min:{field:'pop'}},'EXPR$1':{max:" + + "{field:'pop'}}}")) + .returns("EXPR$0=21; EXPR$1=112047\n"); + + calciteAssert() + .query("select min(pop) as min1, max(pop) as max1 from zips") + .returns("min1=21; max1=112047\n"); + + calciteAssert() + .query("select count(*), max(pop), min(pop), sum(pop), avg(pop) from zips") + .returns("EXPR$0=149; EXPR$1=112047; EXPR$2=21; EXPR$3=7865489; EXPR$4=52788\n"); + } + + @Test + public void groupBy() { + // ascending + calciteAssert() + .query("select min(pop), max(pop), state from zips group by state " + + "order by state limit 3") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0", + "aggregations:{'g_state':{terms:{field:'state',missing:'__MISSING__',size:3," + + " order:{'_key':'asc'}}", + "aggregations:{'EXPR$0':{min:{field:'pop'}},'EXPR$1':{max:{field:'pop'}}}}}")) + .returnsOrdered("EXPR$0=23238; EXPR$1=32383; state=AK", + "EXPR$0=42124; EXPR$1=44165; state=AL", + "EXPR$0=37428; EXPR$1=53532; state=AR"); + + // just one aggregation function + calciteAssert() + .query("select min(pop), state from zips group by state" + + " order by state limit 3") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0", + "aggregations:{'g_state':{terms:{field:'state',missing:'__MISSING__'," + + "size:3, order:{'_key':'asc'}}", + "aggregations:{'EXPR$0':{min:{field:'pop'}} }}}")) + .returnsOrdered("EXPR$0=23238; state=AK", + "EXPR$0=42124; state=AL", + "EXPR$0=37428; state=AR"); + + // group by count + calciteAssert() + .query("select count(city), state from zips group by state " + + "order by state limit 3") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0", + "aggregations:{'g_state':{terms:{field:'state',missing:'__MISSING__'," + + " size:3, order:{'_key':'asc'}}", + "aggregations:{'EXPR$0':{'value_count':{field:'city'}} }}}")) + .returnsOrdered("EXPR$0=3; state=AK", + "EXPR$0=3; state=AL", + "EXPR$0=3; state=AR"); + + // descending + calciteAssert() + .query("select min(pop), max(pop), state from zips group by state " + + " order by state desc limit 3") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0", + "aggregations:{'g_state':{terms:{field:'state',missing:'__MISSING__'," + + "size:3, order:{'_key':'desc'}}", + "aggregations:{'EXPR$0':{min:{field:'pop'}},'EXPR$1':" + + "{max:{field:'pop'}}}}}")) + .returnsOrdered("EXPR$0=25968; EXPR$1=33107; state=WY", + "EXPR$0=45196; EXPR$1=70185; state=WV", + "EXPR$0=51008; EXPR$1=57187; state=WI"); + } + + /** + * Checks + * <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html">Cardinality</a> + * aggregation {@code approx_count_distinct} + */ + @Test + @Ignore + public void approximateCount() throws Exception { + // approx_count_distinct is converted into two aggregations. needs investigation + // ElasticsearchAggregate(group=[{1}], EXPR$0=[COUNT($0)])\r + // ElasticsearchAggregate(group=[{0, 1}])\r + calciteAssert() + .query("select approx_count_distinct(city), state from zips group by state " + + "order by state limit 3") + .queryContains( + ElasticsearchChecker.elasticsearchChecker("'_source':false", + "size:0", + "aggregations:{'g_state':{terms:{field:state, size:3, " + + "order:{'_key':'asc'}}", + "aggregations:{'EXPR$0':{cardinality:{field:city}} }}}")) + .returnsOrdered("EXPR$0=3; state=AK", + "EXPR$0=3; state=AL", + "EXPR$0=3; state=AR"); + + } + } // End ElasticSearchAdapterTest.java http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticsearchJsonTest.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticsearchJsonTest.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticsearchJsonTest.java new file mode 100644 index 0000000..0f2bf0c --- /dev/null +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ElasticsearchJsonTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.elasticsearch; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * Testing correct parsing of JSON (elasticsearch) response. + */ +public class ElasticsearchJsonTest { + + private ObjectMapper mapper; + + @Before + public void setUp() throws Exception { + this.mapper = new ObjectMapper() + .configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) + .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); + } + + @Test + public void aggEmpty() throws Exception { + String json = "{}"; + + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + assertNotNull(a); + assertThat(a.asList().size(), is(0)); + assertThat(a.asMap().size(), is(0)); + } + + @Test + public void aggSingle1() throws Exception { + String json = "{agg1: {value: '111'}}"; + + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + assertNotNull(a); + assertEquals(1, a.asList().size()); + assertEquals(1, a.asMap().size()); + assertEquals("agg1", a.asList().get(0).getName()); + assertEquals("agg1", a.asMap().keySet().iterator().next()); + assertEquals("111", ((ElasticsearchJson.MultiValue) a.asList().get(0)).value()); + + List<Map<String, Object>> rows = new ArrayList<>(); + ElasticsearchJson.visitValueNodes(a, rows::add); + assertThat(rows.size(), is(1)); + assertThat(rows.get(0).get("agg1"), is("111")); + } + + @Test + public void aggMultiValues() throws Exception { + String json = "{ agg1: {min: 0, max: 2, avg: 2.33}}"; + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + assertNotNull(a); + assertEquals(1, a.asList().size()); + assertEquals(1, a.asMap().size()); + assertEquals("agg1", a.asList().get(0).getName()); + + Map<String, Object> values = ((ElasticsearchJson.MultiValue) a.get("agg1")).values(); + assertThat(values.keySet(), hasItems("min", "max", "avg")); + } + + @Test + public void aggSingle2() throws Exception { + String json = "{ agg1: {value: 'foo'}, agg2: {value: 42}}"; + + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + assertNotNull(a); + assertEquals(2, a.asList().size()); + assertEquals(2, a.asMap().size()); + assertThat(a.asMap().keySet(), hasItems("agg1", "agg2")); + } + + @Test + public void aggBuckets1() throws Exception { + String json = "{ groupby: {buckets: [{key:'k1', doc_count:0, myagg:{value: 1.1}}," + + " {key:'k2', myagg:{value: 2.2}}] }}"; + + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + + assertThat(a.asMap().keySet(), hasItem("groupby")); + assertThat(a.get("groupby"), instanceOf(ElasticsearchJson.MultiBucketsAggregation.class)); + ElasticsearchJson.MultiBucketsAggregation multi = a.get("groupby"); + assertThat(multi.buckets().size(), is(2)); + assertThat(multi.getName(), is("groupby")); + assertThat(multi.buckets().get(0).key(), is("k1")); + assertThat(multi.buckets().get(0).keyAsString(), is("k1")); + assertThat(multi.buckets().get(1).key(), is("k2")); + assertThat(multi.buckets().get(1).keyAsString(), is("k2")); + } + + @Test + public void aggManyAggregations() throws Exception { + String json = "{groupby:{buckets:[" + + "{key:'k1', a1:{value:1}, a2:{value:2}}," + + "{key:'k2', a1:{value:3}, a2:{value:4}}" + + "]}}"; + + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + ElasticsearchJson.MultiBucketsAggregation multi = a.get("groupby"); + + assertThat(multi.buckets().get(0).getAggregations().asMap().size(), is(2)); + assertThat(multi.buckets().get(0).getName(), is("groupby")); + assertThat(multi.buckets().get(0).key(), is("k1")); + assertThat(multi.buckets().get(0).getAggregations().asMap().keySet(), hasItems("a1", "a2")); + assertThat(multi.buckets().get(1).getAggregations().asMap().size(), is(2)); + assertThat(multi.buckets().get(1).getName(), is("groupby")); + assertThat(multi.buckets().get(1).key(), is("k2")); + assertThat(multi.buckets().get(1).getAggregations().asMap().keySet(), hasItems("a1", "a2")); + List<Map<String, Object>> rows = new ArrayList<>(); + ElasticsearchJson.visitValueNodes(a, rows::add); + assertThat(rows.size(), is(2)); + assertThat(rows.get(0).get("groupby"), is("k1")); + assertThat(rows.get(0).get("a1"), is(1)); + assertThat(rows.get(0).get("a2"), is(2)); + } + + @Test + public void aggMultiBuckets() throws Exception { + String json = "{col1: {buckets: [" + + "{col2: {doc_count:1, buckets:[{key:'k3', max:{value:41}}]}, key:'k1'}," + + "{col2: {buckets:[{key:'k4', max:{value:42}}], doc_count:1}, key:'k2'}" + + "]}}"; + + ElasticsearchJson.Aggregations a = mapper.readValue(json, ElasticsearchJson.Aggregations.class); + assertNotNull(a); + + assertThat(a.asMap().keySet(), hasItem("col1")); + assertThat(a.get("col1"), instanceOf(ElasticsearchJson.MultiBucketsAggregation.class)); + ElasticsearchJson.MultiBucketsAggregation m = a.get("col1"); + assertThat(m.getName(), is("col1")); + assertThat(m.buckets().size(), is(2)); + assertThat(m.buckets().get(0).key(), is("k1")); + assertThat(m.buckets().get(0).getName(), is("col1")); + assertThat(m.buckets().get(0).getAggregations().asMap().keySet(), hasItem("col2")); + assertThat(m.buckets().get(1).key(), is("k2")); + List<Map<String, Object>> rows = new ArrayList<>(); + ElasticsearchJson.visitValueNodes(a, rows::add); + assertThat(rows.size(), is(2)); + + assertThat(rows.get(0).keySet(), hasItems("col1", "col2", "max")); + assertThat(rows.get(0).get("col1"), is("k1")); + assertThat(rows.get(0).get("col2"), is("k3")); + assertThat(rows.get(0).get("max"), is(41)); + + assertThat(rows.get(1).keySet(), hasItems("col1", "col2", "max")); + assertThat(rows.get(1).get("col1"), is("k2")); + assertThat(rows.get(1).get("col2"), is("k4")); + assertThat(rows.get(1).get("max"), is(42)); + } + +} + +// End ElasticsearchJsonTest.java http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/EmbeddedElasticsearchPolicy.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/EmbeddedElasticsearchPolicy.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/EmbeddedElasticsearchPolicy.java index 5cdd82f..bac2e6f 100644 --- a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/EmbeddedElasticsearchPolicy.java +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/EmbeddedElasticsearchPolicy.java @@ -93,7 +93,16 @@ class EmbeddedElasticsearchPolicy extends ExternalResource { } /** - * Creates index in elastic search given mapping. + * Creates index in elastic search given a mapping. Mapping can contain nested fields expressed + * as dots({@code .}). + * + * <p>Example + * <pre> + * {@code + * b.a: long + * b.b: keyword + * } + * </pre> * * @param index index of the index * @param mapping field and field type mapping @@ -103,21 +112,37 @@ class EmbeddedElasticsearchPolicy extends ExternalResource { Objects.requireNonNull(index, "index"); Objects.requireNonNull(mapping, "mapping"); - ObjectNode json = mapper().createObjectNode(); + ObjectNode mappings = mapper().createObjectNode(); + + ObjectNode properties = mappings.with("mappings").with(index).with("properties"); for (Map.Entry<String, String> entry: mapping.entrySet()) { - json.set(entry.getKey(), json.objectNode().put("type", entry.getValue())); + applyMapping(properties, entry.getKey(), entry.getValue()); } - json = (ObjectNode) json.objectNode().set("properties", json); - json = (ObjectNode) json.objectNode().set(index, json); - json = (ObjectNode) json.objectNode().set("mappings", json); - // create index and mapping - final HttpEntity entity = new StringEntity(mapper().writeValueAsString(json), + final HttpEntity entity = new StringEntity(mapper().writeValueAsString(mappings), ContentType.APPLICATION_JSON); restClient().performRequest("PUT", "/" + index, Collections.emptyMap(), entity); } + /** + * Creates nested mappings for an index. This function is called recursively for each level. + * + * @param parent current parent + * @param key field name + * @param type ES mapping type ({@code keyword}, {@code long} etc.) + */ + private static void applyMapping(ObjectNode parent, String key, String type) { + final int index = key.indexOf('.'); + if (index > -1) { + String prefix = key.substring(0, index); + String suffix = key.substring(index + 1, key.length()); + applyMapping(parent.with(prefix).with("properties"), suffix, type); + } else { + parent.with(key).put("type", type); + } + } + void insertDocument(String index, ObjectNode document) throws IOException { Objects.requireNonNull(index, "index"); Objects.requireNonNull(document, "document"); http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/Projection2Test.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/Projection2Test.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/Projection2Test.java new file mode 100644 index 0000000..20d4457 --- /dev/null +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/Projection2Test.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.calcite.adapter.elasticsearch; + +import org.apache.calcite.jdbc.CalciteConnection; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.schema.impl.ViewTable; +import org.apache.calcite.schema.impl.ViewTableMacro; +import org.apache.calcite.test.CalciteAssert; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableMap; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +/** + * Checks renaming of fields (also upper, lower cases) during projections + */ +public class Projection2Test { + + @ClassRule + public static final EmbeddedElasticsearchPolicy NODE = EmbeddedElasticsearchPolicy.create(); + + private static final String NAME = "nested"; + + @BeforeClass + public static void setupInstance() throws Exception { + + final Map<String, String> mappings = ImmutableMap.of("a", "long", + "b.a", "long", "b.b", "long", "b.c.a", "keyword"); + + NODE.createIndex(NAME, mappings); + + String doc = "{'a': 1, 'b':{'a': 2, 'b':'3', 'c':{'a': 'foo'}}}".replace('\'', '"'); + NODE.insertDocument(NAME, (ObjectNode) NODE.mapper().readTree(doc)); + } + + private CalciteAssert.ConnectionFactory newConnectionFactory() { + return new CalciteAssert.ConnectionFactory() { + @Override public Connection createConnection() throws SQLException { + final Connection connection = DriverManager.getConnection("jdbc:calcite:"); + final SchemaPlus root = connection.unwrap(CalciteConnection.class).getRootSchema(); + + root.add("elastic", new ElasticsearchSchema(NODE.restClient(), NODE.mapper(), NAME)); + + // add calcite view programmatically + final String viewSql = String.format(Locale.ROOT, + "select _MAP['a'] AS \"a\", " + + " _MAP['b.a'] AS \"b.a\", " + + " _MAP['b.b'] AS \"b.b\", " + + " _MAP['b.c.a'] AS \"b.c.a\" " + + " from \"elastic\".\"%s\"", NAME); + + ViewTableMacro macro = ViewTable.viewMacro(root, viewSql, + Collections.singletonList("elastic"), Arrays.asList("elastic", "view"), false); + root.add("VIEW", macro); + return connection; + } + }; + } + + @Test + public void projection() { + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select \"a\", \"b.a\", \"b.b\", \"b.c.a\" from view") + .returns("a=1; b.a=2; b.b=3; b.c.a=foo\n"); + } + + @Test + public void projection2() { + String sql = String.format(Locale.ROOT, "select _MAP['a'], _MAP['b.a'], _MAP['b.b'], " + + "_MAP['b.c.a'], _MAP['missing'], _MAP['b.missing'] from \"elastic\".\"%s\"", NAME); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query(sql) + .returns("EXPR$0=1; EXPR$1=2; EXPR$2=3; EXPR$3=foo; EXPR$4=null; EXPR$5=null\n"); + } + +} + +// End Projection2Test.java http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ProjectionTest.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ProjectionTest.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ProjectionTest.java index 9830036..c4f95af 100644 --- a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ProjectionTest.java +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/ProjectionTest.java @@ -69,10 +69,10 @@ public class ProjectionTest { // add calcite view programmatically final String viewSql = String.format(Locale.ROOT, - "select cast(_MAP['A'] AS varchar(2)) AS \"a\", " - + " cast(_MAP['b'] AS varchar(2)) AS \"b\", " - + " cast(_MAP['cCC'] AS varchar(2)) AS \"c\", " - + " cast(_MAP['DDd'] AS varchar(2)) AS \"d\" " + "select cast(_MAP['A'] AS varchar(2)) AS a," + + " cast(_MAP['b'] AS varchar(2)) AS b, " + + " cast(_MAP['cCC'] AS varchar(2)) AS c, " + + " cast(_MAP['DDd'] AS varchar(2)) AS d " + " from \"elastic\".\"%s\"", NAME); ViewTableMacro macro = ViewTable.viewMacro(root, viewSql, @@ -89,8 +89,35 @@ public class ProjectionTest { CalciteAssert.that() .with(newConnectionFactory()) .query("select * from view") - .returns("a=aa; b=bb; c=cc; d=dd\n"); + .returns("A=aa; B=bb; C=cc; D=dd\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select a, b, c, d from view") + .returns("A=aa; B=bb; C=cc; D=dd\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select d, c, b, a from view") + .returns("D=dd; C=cc; B=bb; A=aa\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select a from view") + .returns("A=aa\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select a, b from view") + .returns("A=aa; B=bb\n"); + + CalciteAssert.that() + .with(newConnectionFactory()) + .query("select b, a from view") + .returns("B=bb; A=aa\n"); + } + } // End ProjectionTest.java http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/QueryBuildersTest.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/QueryBuildersTest.java b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/QueryBuildersTest.java index 7f11715..3f3099a 100644 --- a/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/QueryBuildersTest.java +++ b/elasticsearch/src/test/java/org/apache/calcite/adapter/elasticsearch/QueryBuildersTest.java @@ -23,6 +23,13 @@ import org.junit.Test; import java.io.IOException; import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import static org.junit.Assert.assertEquals; @@ -33,14 +40,24 @@ public class QueryBuildersTest { private final ObjectMapper mapper = new ObjectMapper(); + /** + * Test for simple scalar terms (boolean, int etc.) + * @throws Exception not expected + */ @Test public void term() throws Exception { assertEquals("{\"term\":{\"foo\":\"bar\"}}", toJson(QueryBuilders.termQuery("foo", "bar"))); + assertEquals("{\"term\":{\"bar\":\"foo\"}}", + toJson(QueryBuilders.termQuery("bar", "foo"))); + assertEquals("{\"term\":{\"foo\":\"A\"}}", + toJson(QueryBuilders.termQuery("foo", 'A'))); assertEquals("{\"term\":{\"foo\":true}}", toJson(QueryBuilders.termQuery("foo", true))); assertEquals("{\"term\":{\"foo\":false}}", toJson(QueryBuilders.termQuery("foo", false))); + assertEquals("{\"term\":{\"foo\":0}}", + toJson(QueryBuilders.termQuery("foo", (byte) 0))); assertEquals("{\"term\":{\"foo\":123}}", toJson(QueryBuilders.termQuery("foo", (long) 123))); assertEquals("{\"term\":{\"foo\":41}}", @@ -49,6 +66,46 @@ public class QueryBuildersTest { toJson(QueryBuilders.termQuery("foo", 42.42D))); assertEquals("{\"term\":{\"foo\":1.1}}", toJson(QueryBuilders.termQuery("foo", 1.1F))); + assertEquals("{\"term\":{\"foo\":1}}", + toJson(QueryBuilders.termQuery("foo", new BigDecimal(1)))); + assertEquals("{\"term\":{\"foo\":121}}", + toJson(QueryBuilders.termQuery("foo", new BigInteger("121")))); + assertEquals("{\"term\":{\"foo\":111}}", + toJson(QueryBuilders.termQuery("foo", new AtomicLong(111)))); + assertEquals("{\"term\":{\"foo\":222}}", + toJson(QueryBuilders.termQuery("foo", new AtomicInteger(222)))); + assertEquals("{\"term\":{\"foo\":true}}", + toJson(QueryBuilders.termQuery("foo", new AtomicBoolean(true)))); + } + + @Test + public void terms() throws Exception { + assertEquals("{\"terms\":{\"foo\":[]}}", + toJson(QueryBuilders.termsQuery("foo", Collections.emptyList()))); + + assertEquals("{\"terms\":{\"bar\":[]}}", + toJson(QueryBuilders.termsQuery("bar", Collections.emptySet()))); + + assertEquals("{\"terms\":{\"singleton\":[0]}}", + toJson(QueryBuilders.termsQuery("singleton", Collections.singleton(0)))); + + assertEquals("{\"terms\":{\"foo\":[true]}}", + toJson(QueryBuilders.termsQuery("foo", Collections.singleton(true)))); + + assertEquals("{\"terms\":{\"foo\":[\"bar\"]}}", + toJson(QueryBuilders.termsQuery("foo", Collections.singleton("bar")))); + + assertEquals("{\"terms\":{\"foo\":[\"bar\"]}}", + toJson(QueryBuilders.termsQuery("foo", Collections.singletonList("bar")))); + + assertEquals("{\"terms\":{\"foo\":[true,false]}}", + toJson(QueryBuilders.termsQuery("foo", Arrays.asList(true, false)))); + + assertEquals("{\"terms\":{\"foo\":[1,2,3]}}", + toJson(QueryBuilders.termsQuery("foo", Arrays.asList(1, 2, 3)))); + + assertEquals("{\"terms\":{\"foo\":[1.1,2.2,3.3]}}", + toJson(QueryBuilders.termsQuery("foo", Arrays.asList(1.1, 2.2, 3.3)))); } @Test @@ -109,6 +166,12 @@ public class QueryBuildersTest { toJson(QueryBuilders.rangeQuery("f").lt(1).lt(2).lte(3))); } + @Test + public void matchAll() throws IOException { + assertEquals("{\"match_all\":{}}", + toJson(QueryBuilders.matchAll())); + } + private String toJson(QueryBuilders.QueryBuilder builder) throws IOException { StringWriter writer = new StringWriter(); JsonGenerator gen = mapper.getFactory().createGenerator(writer); http://git-wip-us.apache.org/repos/asf/calcite/blob/79af1c9b/elasticsearch/src/test/java/org/apache/calcite/test/ElasticsearchChecker.java ---------------------------------------------------------------------- diff --git a/elasticsearch/src/test/java/org/apache/calcite/test/ElasticsearchChecker.java b/elasticsearch/src/test/java/org/apache/calcite/test/ElasticsearchChecker.java index 823d4f1..0fe5ebc 100644 --- a/elasticsearch/src/test/java/org/apache/calcite/test/ElasticsearchChecker.java +++ b/elasticsearch/src/test/java/org/apache/calcite/test/ElasticsearchChecker.java @@ -16,15 +16,33 @@ */ package org.apache.calcite.test; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; /** * Internal util methods for ElasticSearch tests */ public class ElasticsearchChecker { + private static final ObjectMapper MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES) // user-friendly settings to + .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); // avoid too much quoting + private ElasticsearchChecker() {} @@ -35,13 +53,75 @@ public class ElasticsearchChecker { */ public static Consumer<List> elasticsearchChecker(final String... strings) { Objects.requireNonNull(strings, "strings"); - return actual -> { - Object[] actualArray = actual == null || actual.isEmpty() ? null - : ((List) actual.get(0)).toArray(); - CalciteAssert.assertArrayEqual("expected Elasticsearch query not found", strings, - actualArray); + return a -> { + ObjectNode actual = a == null || a.isEmpty() ? null + : ((ObjectNode) a.get(0)); + + actual = expandDots(actual); + try { + + String json = "{" + Arrays.stream(strings).collect(Collectors.joining(",")) + "}"; + ObjectNode expected = (ObjectNode) MAPPER.readTree(json); + expected = expandDots(expected); + + if (!expected.equals(actual)) { + assertEquals("expected and actual Elasticsearch queries do not match", + MAPPER.writeValueAsString(expected), + MAPPER.writeValueAsString(actual)); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } }; } + + /** + * Expands attributes with dots ({@code .}) into sub-nodes. + * Use for more friendly JSON format: + * + * <pre> + * {'a.b.c': 1} + * expanded to + * {a: { b: {c: 1}}}} + * </pre> + * @param parent current node + * @param <T> type of node (usually JsonNode). + * @return copy of existing node with field {@code a.b.c} expanded. + */ + @SuppressWarnings("unchecked") + private static <T extends JsonNode> T expandDots(T parent) { + Objects.requireNonNull(parent, "parent"); + + if (parent.isValueNode()) { + return parent.deepCopy(); + } + + // ArrayNode + if (parent.isArray()) { + ArrayNode arr = (ArrayNode) parent; + ArrayNode copy = arr.arrayNode(); + arr.elements().forEachRemaining(e -> copy.add(expandDots(e))); + return (T) copy; + } + + // ObjectNode + ObjectNode objectNode = (ObjectNode) parent; + final ObjectNode copy = objectNode.objectNode(); + objectNode.fields().forEachRemaining(e -> { + final String property = e.getKey(); + final JsonNode node = e.getValue(); + + final String[] names = property.split("\\."); + ObjectNode copy2 = copy; + for (int i = 0; i < names.length - 1; i++) { + copy2 = copy2.with(names[i]); + } + copy2.set(names[names.length - 1], expandDots(node)); + }); + + return (T) copy; + } + } // End ElasticsearchChecker.java
