/**
* 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.metamodel.elasticsearch.nativeclient;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.table.TableModel;
import org.apache.metamodel.MetaModelHelper;
import org.apache.metamodel.UpdateCallback;
import org.apache.metamodel.UpdateScript;
import org.apache.metamodel.UpdateableDataContext;
import org.apache.metamodel.create.CreateTable;
import org.apache.metamodel.data.DataSet;
import org.apache.metamodel.data.DataSetTableModel;
import org.apache.metamodel.data.InMemoryDataSet;
import org.apache.metamodel.data.Row;
import org.apache.metamodel.delete.DeleteFrom;
import org.apache.metamodel.drop.DropTable;
import org.apache.metamodel.elasticsearch.common.ElasticSearchUtils;
import org.apache.metamodel.elasticsearch.nativeclient.utils.EmbeddedElasticsearchServer;
import org.apache.metamodel.query.FunctionType;
import org.apache.metamodel.query.Query;
import org.apache.metamodel.query.SelectItem;
import org.apache.metamodel.query.parser.QueryParserException;
import org.apache.metamodel.schema.Column;
import org.apache.metamodel.schema.ColumnType;
import org.apache.metamodel.schema.Schema;
import org.apache.metamodel.schema.Table;
import org.apache.metamodel.update.Update;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.mapping.delete.DeleteMappingRequestBuilder;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequestBuilder;
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.IndicesAdminClient;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class ElasticSearchDataContextTest {
private static final String indexName = "twitter";
private static final String indexType1 = "tweet1";
private static final String indexType2 = "tweet2";
private static final String indexName2 = "twitter2";
private static final String indexType3 = "tweet3";
private static final String bulkIndexType = "bulktype";
private static final String peopleIndexType = "peopletype";
private static final String mapping = "{\"date_detection\":\"false\",\"properties\":{\"message\":{\"type\":\"string\",\"index\":\"not_analyzed\",\"doc_values\":\"true\"}}}";
private static EmbeddedElasticsearchServer embeddedElasticsearchServer;
private static Client client;
private static UpdateableDataContext dataContext;
@BeforeClass
public static void beforeTests() throws Exception {
embeddedElasticsearchServer = new EmbeddedElasticsearchServer();
client = embeddedElasticsearchServer.getClient();
indexTweeterDocument(indexType1, 1);
indexTweeterDocument(indexType2, 1);
indexTweeterDocument(indexType2, 2, null);
insertPeopleDocuments();
indexTweeterDocument(indexType2, 1);
indexBulkDocuments(indexName, bulkIndexType, 10);
// The refresh API allows to explicitly refresh one or more index,
// making all operations performed since the last refresh available for
// search
embeddedElasticsearchServer.getClient().admin().indices().prepareRefresh().execute().actionGet();
dataContext = new ElasticSearchDataContext(client, indexName);
System.out.println("Embedded ElasticSearch server created!");
}
private static void insertPeopleDocuments() throws IOException {
indexOnePeopleDocument("female", 20, 5);
indexOnePeopleDocument("female", 17, 8);
indexOnePeopleDocument("female", 18, 9);
indexOnePeopleDocument("female", 19, 10);
indexOnePeopleDocument("female", 20, 11);
indexOnePeopleDocument("male", 19, 1);
indexOnePeopleDocument("male", 17, 2);
indexOnePeopleDocument("male", 18, 3);
indexOnePeopleDocument("male", 18, 4);
}
@AfterClass
public static void afterTests() {
embeddedElasticsearchServer.shutdown();
System.out.println("Embedded ElasticSearch server shut down!");
}
@Test
public void testSimpleQuery() throws Exception {
assertEquals("[bulktype, peopletype, tweet1, tweet2]",
Arrays.toString(dataContext.getDefaultSchema().getTableNames()));
Table table = dataContext.getDefaultSchema().getTableByName("tweet1");
assertEquals("[_id, message, postDate, user]", Arrays.toString(table.getColumnNames()));
assertEquals(ColumnType.STRING, table.getColumnByName("user").getType());
assertEquals(ColumnType.DATE, table.getColumnByName("postDate").getType());
assertEquals(ColumnType.BIGINT, table.getColumnByName("message").getType());
try(DataSet ds = dataContext.query().from(indexType1).select("user").and("message").execute()) {
assertEquals(ElasticSearchDataSet.class, ds.getClass());
assertTrue(ds.next());
assertEquals("Row[values=[user1, 1]]", ds.getRow().toString());
}
}
@Test
public void testDocumentIdAsPrimaryKey() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName("tweet2");
Column[] pks = table.getPrimaryKeys();
assertEquals(1, pks.length);
assertEquals("_id", pks[0].getName());
try (DataSet ds = dataContext.query().from(table).select("user", "_id").orderBy("_id").asc().execute()) {
assertTrue(ds.next());
assertEquals("Row[values=[user1, tweet_tweet2_1]]", ds.getRow().toString());
}
}
@Test
public void testExecutePrimaryKeyLookupQuery() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName("tweet2");
Column[] pks = table.getPrimaryKeys();
try (DataSet ds = dataContext.query().from(table).selectAll().where(pks[0]).eq("tweet_tweet2_1").execute()) {
assertTrue(ds.next());
Object dateValue = ds.getRow().getValue(2);
assertEquals("Row[values=[tweet_tweet2_1, 1, " + dateValue + ", user1]]", ds.getRow().toString());
assertFalse(ds.next());
assertEquals(InMemoryDataSet.class, ds.getClass());
}
}
@Test
public void testDateIsHandledAsDate() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName("tweet1");
Column column = table.getColumnByName("postDate");
ColumnType type = column.getType();
assertEquals(ColumnType.DATE, type);
DataSet dataSet = dataContext.query().from(table).select(column).execute();
while (dataSet.next()) {
Object value = dataSet.getRow().getValue(column);
assertTrue("Got class: " + value.getClass() + ", expected Date (or subclass)", value instanceof Date);
}
}
@Test
public void testNumberIsHandledAsNumber() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName(peopleIndexType);
Column column = table.getColumnByName("age");
ColumnType type = column.getType();
assertEquals(ColumnType.BIGINT, type);
DataSet dataSet = dataContext.query().from(table).select(column).execute();
while (dataSet.next()) {
Object value = dataSet.getRow().getValue(column);
assertTrue("Got class: " + value.getClass() + ", expected Number (or subclass)", value instanceof Number);
}
}
@Test
public void testCreateTableInsertQueryAndDrop() throws Exception {
final Schema schema = dataContext.getDefaultSchema();
final CreateTable createTable = new CreateTable(schema, "testCreateTable");
createTable.withColumn("foo").ofType(ColumnType.STRING);
createTable.withColumn("bar").ofType(ColumnType.NUMBER);
dataContext.executeUpdate(createTable);
final Table table = schema.getTableByName("testCreateTable");
assertEquals("[" + ElasticSearchUtils.FIELD_ID + ", foo, bar]", Arrays.toString(table.getColumnNames()));
final Column fooColumn = table.getColumnByName("foo");
final Column idColumn = table.getPrimaryKeys()[0];
assertEquals("Column[name=_id,columnNumber=0,type=STRING,nullable=null,nativeType=null,columnSize=null]",
idColumn.toString());
dataContext.executeUpdate(new UpdateScript() {
@Override
public void run(UpdateCallback callback) {
callback.insertInto(table).value("foo", "hello").value("bar", 42).execute();
callback.insertInto(table).value("foo", "world").value("bar", 43).execute();
}
});
dataContext.refreshSchemas();
try (DataSet ds = dataContext.query().from(table).selectAll().orderBy("bar").execute()) {
assertTrue(ds.next());
assertEquals("hello", ds.getRow().getValue(fooColumn).toString());
assertNotNull(ds.getRow().getValue(idColumn));
assertTrue(ds.next());
assertEquals("world", ds.getRow().getValue(fooColumn).toString());
assertNotNull(ds.getRow().getValue(idColumn));
assertFalse(ds.next());
}
dataContext.executeUpdate(new DropTable(table));
dataContext.refreshSchemas();
assertNull(dataContext.getTableByQualifiedLabel(table.getName()));
}
@Test
public void testDetectOutsideChanges() throws Exception {
ElasticSearchDataContext elasticSearchDataContext = (ElasticSearchDataContext) dataContext;
// Create the type in ES
final IndicesAdminClient indicesAdmin = elasticSearchDataContext.getElasticSearchClient().admin().indices();
final String tableType = "outsideTable";
Object[] sourceProperties = { "testA", "type=string, store=true", "testB", "type=string, store=true" };
new PutMappingRequestBuilder(indicesAdmin).setIndices(indexName).setType(tableType).setSource(sourceProperties)
.execute().actionGet();
dataContext.refreshSchemas();
assertNotNull(dataContext.getDefaultSchema().getTableByName(tableType));
new DeleteMappingRequestBuilder(indicesAdmin).setIndices(indexName).setType(tableType).execute().actionGet();
dataContext.refreshSchemas();
assertNull(dataContext.getTableByQualifiedLabel(tableType));
}
@Test
public void testDeleteAll() throws Exception {
final Schema schema = dataContext.getDefaultSchema();
final CreateTable createTable = new CreateTable(schema, "testCreateTable");
createTable.withColumn("foo").ofType(ColumnType.STRING);
createTable.withColumn("bar").ofType(ColumnType.NUMBER);
dataContext.executeUpdate(createTable);
final Table table = schema.getTableByName("testCreateTable");
dataContext.executeUpdate(new UpdateScript() {
@Override
public void run(UpdateCallback callback) {
callback.insertInto(table).value("foo", "hello").value("bar", 42).execute();
callback.insertInto(table).value("foo", "world").value("bar", 43).execute();
}
});
dataContext.executeUpdate(new DeleteFrom(table));
Row row = MetaModelHelper.executeSingleRowQuery(dataContext, dataContext.query().from(table).selectCount()
.toQuery());
assertEquals("Row[values=[0]]", row.toString());
dataContext.executeUpdate(new DropTable(table));
}
@Test
public void testDeleteByQuery() throws Exception {
final Schema schema = dataContext.getDefaultSchema();
final CreateTable createTable = new CreateTable(schema, "testCreateTable");
createTable.withColumn("foo").ofType(ColumnType.STRING);
createTable.withColumn("bar").ofType(ColumnType.NUMBER);
dataContext.executeUpdate(createTable);
final Table table = schema.getTableByName("testCreateTable");
dataContext.executeUpdate(new UpdateScript() {
@Override
public void run(UpdateCallback callback) {
callback.insertInto(table).value("foo", "hello").value("bar", 42).execute();
callback.insertInto(table).value("foo", "world").value("bar", 43).execute();
}
});
dataContext.executeUpdate(new DeleteFrom(table).where("foo").eq("hello").where("bar").eq(42));
Row row = MetaModelHelper.executeSingleRowQuery(dataContext,
dataContext.query().from(table).select("foo", "bar").toQuery());
assertEquals("Row[values=[world, 43]]", row.toString());
dataContext.executeUpdate(new DropTable(table));
}
@Test
public void testDeleteUnsupportedQueryType() throws Exception {
final Schema schema = dataContext.getDefaultSchema();
final CreateTable createTable = new CreateTable(schema, "testCreateTable");
createTable.withColumn("foo").ofType(ColumnType.STRING);
createTable.withColumn("bar").ofType(ColumnType.NUMBER);
dataContext.executeUpdate(createTable);
final Table table = schema.getTableByName("testCreateTable");
try {
dataContext.executeUpdate(new UpdateScript() {
@Override
public void run(UpdateCallback callback) {
callback.insertInto(table).value("foo", "hello").value("bar", 42).execute();
callback.insertInto(table).value("foo", "world").value("bar", 43).execute();
}
});
// greater than is not yet supported
try {
dataContext.executeUpdate(new DeleteFrom(table).where("bar").gt(40));
fail("Exception expected");
} catch (UnsupportedOperationException e) {
assertEquals("Could not push down WHERE items to delete by query request: [testCreateTable.bar > 40]",
e.getMessage());
}
} finally {
dataContext.executeUpdate(new DropTable(table));
}
}
@Test
public void testUpdateRow() throws Exception {
final Schema schema = dataContext.getDefaultSchema();
final CreateTable createTable = new CreateTable(schema, "testCreateTable");
createTable.withColumn("foo").ofType(ColumnType.STRING);
createTable.withColumn("bar").ofType(ColumnType.NUMBER);
dataContext.executeUpdate(createTable);
final Table table = schema.getTableByName("testCreateTable");
try {
dataContext.executeUpdate(new UpdateScript() {
@Override
public void run(UpdateCallback callback) {
callback.insertInto(table).value("foo", "hello").value("bar", 42).execute();
callback.insertInto(table).value("foo", "world").value("bar", 43).execute();
}
});
dataContext.executeUpdate(new Update(table).value("foo", "howdy").where("bar").eq(42));
DataSet dataSet = dataContext.query().from(table).select("foo", "bar").orderBy("bar").execute();
assertTrue(dataSet.next());
assertEquals("Row[values=[howdy, 42]]", dataSet.getRow().toString());
assertTrue(dataSet.next());
assertEquals("Row[values=[world, 43]]", dataSet.getRow().toString());
assertFalse(dataSet.next());
dataSet.close();
} finally {
dataContext.executeUpdate(new DropTable(table));
}
}
@Test
public void testDropTable() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName(peopleIndexType);
// assert that the table was there to begin with
{
DataSet ds = dataContext.query().from(table).selectCount().execute();
ds.next();
assertEquals("Row[values=[9]]", ds.getRow().toString());
ds.close();
}
dataContext.executeUpdate(new DropTable(table));
try {
DataSet ds = dataContext.query().from(table).selectCount().execute();
ds.next();
assertEquals("Row[values=[0]]", ds.getRow().toString());
ds.close();
} finally {
// restore the people documents for the next tests
insertPeopleDocuments();
client.admin().indices().prepareRefresh().execute().actionGet();
dataContext = new ElasticSearchDataContext(client, indexName);
}
}
@Test
public void testWhereColumnEqualsValues() throws Exception {
try (DataSet ds = dataContext.query().from(bulkIndexType).select("user").and("message").where("user")
.isEquals("user4").execute()) {
assertEquals(ElasticSearchDataSet.class, ds.getClass());
assertTrue(ds.next());
assertEquals("Row[values=[user4, 4]]", ds.getRow().toString());
assertFalse(ds.next());
}
}
@Test
public void testWhereColumnIsNullValues() throws Exception {
try(DataSet ds = dataContext.query().from(indexType2).select("message").where("postDate")
.isNull().execute()){
assertEquals(ElasticSearchDataSet.class, ds.getClass());
assertTrue(ds.next());
assertEquals("Row[values=[2]]", ds.getRow().toString());
assertFalse(ds.next());
}
}
@Test
public void testWhereColumnIsNotNullValues() throws Exception {
try(DataSet ds = dataContext.query().from(indexType2).select("message").where("postDate")
.isNotNull().execute()){
assertEquals(ElasticSearchDataSet.class, ds.getClass());
assertTrue(ds.next());
assertEquals("Row[values=[1]]", ds.getRow().toString());
assertFalse(ds.next());
}
}
@Test
public void testWhereMultiColumnsEqualValues() throws Exception {
try(DataSet ds = dataContext.query().from(bulkIndexType).select("user").and("message").where("user")
.isEquals("user4").and("message").ne(5).execute()){
assertEquals(ElasticSearchDataSet.class, ds.getClass());
assertTrue(ds.next());
assertEquals("Row[values=[user4, 4]]", ds.getRow().toString());
assertFalse(ds.next());
}
}
@Test
public void testWhereColumnInValues() throws Exception {
try (DataSet ds = dataContext.query().from(bulkIndexType).select("user").and("message").where("user")
.in("user4", "user5").orderBy("message").execute()) {
assertTrue(ds.next());
String row1 = ds.getRow().toString();
assertEquals("Row[values=[user4, 4]]", row1);
assertTrue(ds.next());
String row2 = ds.getRow().toString();
assertEquals("Row[values=[user5, 5]]", row2);
assertFalse(ds.next());
}
}
@Test
public void testGroupByQuery() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName(peopleIndexType);
Query q = new Query();
q.from(table);
q.groupBy(table.getColumnByName("gender"));
q.select(new SelectItem(table.getColumnByName("gender")),
new SelectItem(FunctionType.MAX, table.getColumnByName("age")),
new SelectItem(FunctionType.MIN, table.getColumnByName("age")), new SelectItem(FunctionType.COUNT, "*",
"total"), new SelectItem(FunctionType.MIN, table.getColumnByName("id")).setAlias("firstId"));
q.orderBy("gender");
DataSet data = dataContext.executeQuery(q);
assertEquals(
"[peopletype.gender, MAX(peopletype.age), MIN(peopletype.age), COUNT(*) AS total, MIN(peopletype.id) AS firstId]",
Arrays.toString(data.getSelectItems()));
assertTrue(data.next());
assertEquals("Row[values=[female, 20, 17, 5, 5]]", data.getRow().toString());
assertTrue(data.next());
assertEquals("Row[values=[male, 19, 17, 4, 1]]", data.getRow().toString());
assertFalse(data.next());
}
@Test
public void testFilterOnNumberColumn() {
Table table = dataContext.getDefaultSchema().getTableByName(bulkIndexType);
Query q = dataContext.query().from(table).select("user").where("message").greaterThan(7).toQuery();
DataSet data = dataContext.executeQuery(q);
String[] expectations = new String[] { "Row[values=[user8]]", "Row[values=[user9]]" };
assertTrue(data.next());
assertTrue(Arrays.asList(expectations).contains(data.getRow().toString()));
assertTrue(data.next());
assertTrue(Arrays.asList(expectations).contains(data.getRow().toString()));
assertFalse(data.next());
}
@Test
public void testMaxRows() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName(peopleIndexType);
Query query = new Query().from(table).select(table.getColumns()).setMaxRows(5);
DataSet dataSet = dataContext.executeQuery(query);
TableModel tableModel = new DataSetTableModel(dataSet);
assertEquals(5, tableModel.getRowCount());
}
@Test
public void testCountQuery() throws Exception {
Table table = dataContext.getDefaultSchema().getTableByName(bulkIndexType);
Query q = new Query().selectCount().from(table);
List<Object[]> data = dataContext.executeQuery(q).toObjectArrays();
assertEquals(1, data.size());
Object[] row = data.get(0);
assertEquals(1, row.length);
assertEquals("[10]", Arrays.toString(row));
}
@Test(expected = IllegalArgumentException.class)
public void testQueryForANonExistingTable() throws Exception {
dataContext.query().from("nonExistingTable").select("user").and("message").execute();
}
@Test(expected = QueryParserException.class)
public void testQueryForAnExistingTableAndNonExistingField() throws Exception {
indexTweeterDocument(indexType1, 1);
dataContext.query().from(indexType1).select("nonExistingField").execute();
}
@Test
public void testNonDynamicMapingTableNames() throws Exception {
createIndex();
ElasticSearchDataContext dataContext2 = new ElasticSearchDataContext(client, indexName2);
assertEquals("[tweet3]", Arrays.toString(dataContext2.getDefaultSchema().getTableNames()));
}
private static void createIndex() {
CreateIndexRequest cir = new CreateIndexRequest(indexName2);
CreateIndexResponse response = client.admin().indices().create(cir).actionGet();
System.out.println("create index: " + response.isAcknowledged());
PutMappingRequest pmr = new PutMappingRequest(indexName2).type(indexType3).source(mapping);
PutMappingResponse response2 = client.admin().indices().putMapping(pmr).actionGet();
System.out.println("put mapping: " + response2.isAcknowledged());
}
private static void indexBulkDocuments(String indexName, String indexType, int numberOfDocuments) {
BulkRequestBuilder bulkRequest = client.prepareBulk();
for (int i = 0; i < numberOfDocuments; i++) {
bulkRequest.add(client.prepareIndex(indexName, indexType, Integer.toString(i)).setSource(
buildTweeterJson(i)));
}
bulkRequest.execute().actionGet();
}
private static void indexTweeterDocument(String indexType, int id, Date date) {
client.prepareIndex(indexName, indexType).setSource(buildTweeterJson(id, date))
.setId("tweet_" + indexType + "_" + id).execute().actionGet();
}
private static void indexTweeterDocument(String indexType, int id) {
client.prepareIndex(indexName, indexType).setSource(buildTweeterJson(id))
.setId("tweet_" + indexType + "_" + id).execute().actionGet();
}
private static void indexOnePeopleDocument(String gender, int age, int id) throws IOException {
client.prepareIndex(indexName, peopleIndexType).setSource(buildPeopleJson(gender, age, id)).execute()
.actionGet();
}
private static Map<String, Object> buildTweeterJson(int elementId) {
return buildTweeterJson(elementId, new Date());
}
private static Map<String, Object> buildTweeterJson(int elementId, Date date) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("user", "user" + elementId);
map.put("postDate", date);
map.put("message", elementId);
return map;
}
private static XContentBuilder buildPeopleJson(String gender, int age, int elementId) throws IOException {
return jsonBuilder().startObject().field("gender", gender).field("age", age).field("id", elementId).endObject();
}
}