/** * Licensed 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 io.horizondb.db.parser; import io.horizondb.db.Configuration; import io.horizondb.db.HorizonDBException; import io.horizondb.db.databases.Database; import io.horizondb.db.databases.DatabaseManager; import io.horizondb.db.databases.InMemoryDatabaseManager; import io.horizondb.db.series.InMemoryTimeSeriesManager; import io.horizondb.db.series.InMemoryTimeSeriesPartitionManager; import io.horizondb.db.series.TimeSeriesManager; import io.horizondb.db.series.TimeSeriesPartitionManager; import io.horizondb.io.ReadableBuffer; import io.horizondb.model.core.iterators.BinaryTimeSeriesRecordIterator; import io.horizondb.model.core.records.BinaryTimeSeriesRecord; import io.horizondb.model.protocol.CreateDatabasePayload; import io.horizondb.model.protocol.CreateTimeSeriesPayload; import io.horizondb.model.protocol.DropDatabasePayload; import io.horizondb.model.protocol.DropTimeSeriesPayload; import io.horizondb.model.protocol.HqlQueryPayload; import io.horizondb.model.protocol.InsertPayload; import io.horizondb.model.protocol.Msg; import io.horizondb.model.protocol.OpCode; import io.horizondb.model.protocol.SelectPayload; import io.horizondb.model.protocol.UseDatabasePayload; import io.horizondb.model.schema.DatabaseDefinition; import io.horizondb.model.schema.RecordSetDefinition; import io.horizondb.model.schema.RecordTypeDefinition; import io.horizondb.model.schema.TimeSeriesDefinition; import java.io.IOException; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import static io.horizondb.model.core.util.TimeUtils.EUROPE_BERLIN_TIMEZONE; import static io.horizondb.model.core.util.TimeUtils.parseDateTime; import static io.horizondb.test.AssertExceptions.assertErrorMessageContains; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** */ public class QueryParserTest { /** * The database configuration used during the tests */ private Configuration configuration; /** * The database manager used during the tests */ private DatabaseManager databaseManager; @Before public void setUp() throws IOException, InterruptedException { this.configuration = newConfiguration(); this.databaseManager = newDatabaseManager(this.configuration); this.databaseManager.start(); } @After public void tearDown() throws InterruptedException { this.databaseManager.shutdown(); this.databaseManager = null; this.configuration = null; } @Test public void testParseCreateDatabase() throws HorizonDBException, IOException { Msg<CreateDatabasePayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", " CREATE DATABASE TEST;")); assertNotNull(msg); msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "CREATE DATABASE test ;")); assertNotNull(msg); assertEquals(new DatabaseDefinition("test"), msg.getPayload().getDefinition()); } @Test public void testParseUseDatabase() throws HorizonDBException, IOException { Msg<UseDatabasePayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "USE TEST;")); assertNotNull(msg); assertEquals("TEST", msg.getPayload().getDatabase()); } @Test public void testParseCreateTimeSeries() throws HorizonDBException, IOException { TimeSeriesDefinition expected = getTimeSeriesDefinition(); createDatabase(); Msg<CreateTimeSeriesPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "CREATE TIMESERIES Dax (" + "Quote(bidPrice DECIMAL, askPrice DECIMAL, bidVolume INTEGER, askVolume INTEGER), " + "Trade(price DECIMAL, volume INTEGER))TIME_UNIT = NANOSECONDS TIMEZONE = 'Europe/Berlin';")); assertNotNull(msg); assertEquals(expected, msg.getPayload().getDefinition()); } @Test public void testParseDropDatabase() throws HorizonDBException, IOException { createDatabase(); Msg<DropDatabasePayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "DROP DATABASE test;")); assertNotNull(msg); assertEquals("test", msg.getPayload().getDatabase()); } @Test public void testParseDropDatabaseWithCurrentDatabase() throws HorizonDBException, IOException { createDatabase(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "DROP DATABASE test;")); fail(); } catch (HorizonDBException e) { String msgFragment = "Cannot drop test as it is the currently used database." + " You must first switch to an other database."; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseDropTimeSeries() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<DropTimeSeriesPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "DROP TIMESERIES Dax;")); assertNotNull(msg); assertEquals("test", msg.getPayload().getDatabase()); assertEquals("Dax", msg.getPayload().getTimeSeries()); } @Test public void testParseDropTimeSeriesWithDatabaseNameSpecified() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<DropTimeSeriesPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "DROP TIMESERIES test.Dax;")); assertNotNull(msg); assertEquals("test", msg.getPayload().getDatabase()); assertEquals("Dax", msg.getPayload().getTimeSeries()); } @Test public void testParseCreateTimeSeriesWithDatabaseNameSpecifiedInTheQuery() throws HorizonDBException, IOException { TimeSeriesDefinition expected = getTimeSeriesDefinition(); createDatabase(); Msg<CreateTimeSeriesPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "CREATE TIMESERIES test.Dax (" + "Quote(bidPrice DECIMAL, askPrice DECIMAL, bidVolume INTEGER, askVolume INTEGER), " + "Trade(price DECIMAL, volume INTEGER))TIME_UNIT = NANOSECONDS TIMEZONE = 'Europe/Berlin';")); assertNotNull(msg); assertEquals(expected, msg.getPayload().getDefinition()); } @Test public void testParseCreateTimeSeriesWithDefaultTimeUnit() throws HorizonDBException, IOException { createDatabase(); RecordTypeDefinition quote = RecordTypeDefinition.newBuilder("Quote") .addNanosecondTimestampField("received") .addDecimalField("bidPrice") .addDecimalField("askPrice") .addIntegerField("bidVolume") .addIntegerField("askVolume") .build(); RecordTypeDefinition trade = RecordTypeDefinition.newBuilder("Trade") .addNanosecondTimestampField("received") .addDecimalField("price") .addIntegerField("volume") .build(); TimeSeriesDefinition expected = TimeSeriesDefinition.newBuilder("Dax") .addRecordType(quote) .addRecordType(trade) .timeUnit(TimeUnit.MILLISECONDS) .timeZone(TimeZone.getTimeZone("America/Los_Angeles")) .build(); Msg<CreateTimeSeriesPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "CREATE TIMESERIES Dax (" + "Quote(received NANOSECONDS_TIMESTAMP, bidPrice DECIMAL, askPrice DECIMAL, bidVolume INTEGER, askVolume INTEGER), " + "Trade(received NANOSECONDS_TIMESTAMP, price DECIMAL, volume INTEGER))TIMEZONE = 'America/Los_Angeles';")); assertNotNull(msg); assertEquals(expected, msg.getPayload().getDefinition()); } @Test public void testParseSelectAll() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<SelectPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT * FROM Dax;")); assertEquals("Dax", msg.getPayload().getSeriesName()); assertEquals("", msg.getPayload().getPredicate().toString()); } @Test public void testParseSelectWithTimestampGreaterThan() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp > 2ns"); } @Test public void testParseSelectWithAndAndOr() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp > 2ns AND timestamp < 4ns OR timestamp > 6ns AND timestamp < 8ns"); } @Test public void testParseSelectWithAndOrAndParentheses() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp > 2ns AND (timestamp < 4ns OR timestamp > 6ns)"); } @Test public void testParseSelectWithInExpression() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp IN (2ns, 4ns)"); } @Test public void testParseSelectWithBetweenExpression() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp BETWEEN 2ns AND 4ns"); } @Test public void testParseSelectWithNotBetweenExpression() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp NOT BETWEEN 2ns AND 6ns"); } @Test public void testParseSelectWithBetweenAndInExpression() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp BETWEEN 2ns AND 6ns AND timestamp IN (5ns, 6ns)"); } @Test public void testParseSelectWithNotInExpression() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp NOT IN (2ns, 4ns)"); } @Test public void testParseSelectWithInExpressionAndOnlyOneValue() throws HorizonDBException, IOException { testParseSelectWithPredicate("timestamp IN (2ns)"); } @Test public void testParseInsert() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<InsertPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "INSERT INTO Dax.Trade (timestamp, price, volume) VALUES ('2014-05-23 09:44:30', 125E-1, 10);")); InsertPayload payload = msg.getPayload(); assertEquals("Dax", payload.getSeries()); assertEquals(1, payload.getRecordType()); TimeSeriesDefinition definition = getTimeSeriesDefinition(); ReadableBuffer buffer = payload.getBuffer(); try (BinaryTimeSeriesRecordIterator iterator = new BinaryTimeSeriesRecordIterator(definition, buffer)) { assertTrue(iterator.hasNext()); BinaryTimeSeriesRecord binaryRecord = iterator.next(); assertEquals(parseDateTime(EUROPE_BERLIN_TIMEZONE, "2014-05-23 09:44:30"), binaryRecord.getTimestampInMillis(0)); assertEquals(10L, binaryRecord.getLong(2)); assertFalse(iterator.hasNext()); } } @Test public void testParseInsertWithDatabaseNameSpecifiedInTheQuery() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<InsertPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "INSERT INTO test.Dax.Trade (timestamp, price, volume) VALUES ('2014-05-23 09:44:30', 125E-1, 10);")); InsertPayload payload = msg.getPayload(); assertEquals("Dax", payload.getSeries()); assertEquals(1, payload.getRecordType()); TimeSeriesDefinition definition = getTimeSeriesDefinition(); ReadableBuffer buffer = payload.getBuffer(); try (BinaryTimeSeriesRecordIterator iterator = new BinaryTimeSeriesRecordIterator(definition, buffer)) { assertTrue(iterator.hasNext()); BinaryTimeSeriesRecord binaryRecord = iterator.next(); assertEquals(parseDateTime(EUROPE_BERLIN_TIMEZONE, "2014-05-23 09:44:30"), binaryRecord.getTimestampInMillis(0)); assertEquals(12.5, binaryRecord.getDouble(1), 0.0); assertEquals(10L, binaryRecord.getLong(2)); assertFalse(iterator.hasNext()); } } @Test public void testParseInsertWithoutFieldNames() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<InsertPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "INSERT INTO Dax.Trade VALUES ('2014-05-23 09:44:30', 125E-1, 10);")); InsertPayload payload = msg.getPayload(); assertEquals("Dax", payload.getSeries()); assertEquals(1, payload.getRecordType()); TimeSeriesDefinition definition = getTimeSeriesDefinition(); ReadableBuffer buffer = payload.getBuffer(); try (BinaryTimeSeriesRecordIterator iterator = new BinaryTimeSeriesRecordIterator(definition, buffer)) { assertTrue(iterator.hasNext()); BinaryTimeSeriesRecord binaryRecord = iterator.next(); assertEquals(parseDateTime(EUROPE_BERLIN_TIMEZONE, "2014-05-23 09:44:30"), binaryRecord.getTimestampInMillis(0)); assertEquals(12.5, binaryRecord.getDouble(1), 0.0); assertEquals(10L, binaryRecord.getLong(2)); assertFalse(iterator.hasNext()); } } @Test public void testParseInsertWithInvalidTimeValue() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "INSERT INTO Dax.Trade (timestamp, price, volume) VALUES ('2014.05.23 09:44:30', 125E-1, 10);")); fail(); } catch (BadHqlGrammarException e) { String msgFragment = "The value 2014.05.23 09:44:30 cannot be parsed into a valid date"; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseInsertWithInvalidLongValue() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "INSERT INTO Dax.Trade (timestamp, price, volume) VALUES ('2014-05-23 09:44:30', 125E-1, 'test');")); fail(); } catch (BadHqlGrammarException e) { String msgFragment = "The value 'test' cannot be converted into a number"; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseWithInvalidQuery() throws HorizonDBException, IOException { try { Configuration configuration = newConfiguration(); QueryParser.parse(configuration, newDatabaseManager(configuration), newMsg("", "CREATE DB TEST;")); fail(); } catch (BadHqlGrammarException e) { assertTrue(true); } } @Test public void testParseSelectWithOneRecordProjection() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); TimeSeriesDefinition timeSeriesDefinition = getTimeSeriesDefinition(); Msg<SelectPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT Trade.* FROM Dax;")); assertEquals("Dax", msg.getPayload().getSeriesName()); RecordSetDefinition definition = msg.getPayload().getProjection().getDefinition(timeSeriesDefinition); assertEquals(1, definition.getNumberOfRecordTypes()); assertEquals(timeSeriesDefinition.getRecordType(1), definition.getRecordType(0)); } @Test public void testParseSelectWithDatabaseNameSpecifiedInTheQuery() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<SelectPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("", "SELECT * FROM test.Dax;")); assertEquals("test", msg.getPayload().getDatabaseName()); assertEquals("Dax", msg.getPayload().getSeriesName()); } @Test public void testParseSelectWithEmptyIn() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<SelectPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT * FROM Dax WHERE timestamp IN ();")); assertEquals("test", msg.getPayload().getDatabaseName()); assertEquals("Dax", msg.getPayload().getSeriesName()); } @Test public void testParseSelectWithInvalidLongValue() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT Trade.* FROM Dax WHERE timestamp = 'test';")); fail(); } catch (BadHqlGrammarException e) { String msgFragment = "The format of the date/time: test does not match the expected one: yyyy-MM-dd HH:mm:ss.SSS"; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseSelectWithInvalidFieldName() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT Trade.* FROM Dax WHERE unknown = 'test';")); fail(); } catch (HorizonDBException e) { String msgFragment = "Unknown field: unknown"; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseSelectWithInvalidProjectionRecordName() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT Tarde.priec FROM Dax;")); fail(); } catch (HorizonDBException e) { String msgFragment = "No Tarde records have not been defined within this record set"; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseSelectWithInvalidProjectionFieldName() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT Trade.priec FROM Dax;")); fail(); } catch (HorizonDBException e) { String msgFragment = "the field priec does not exists within the record Trade"; assertErrorMessageContains(msgFragment, e); } } @Test public void testParseSelectWithInvalidProjectionFieldNameMix() throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); try { QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT Trade.price, Trade.* FROM Dax;")); fail(); } catch (HorizonDBException e) { String msgFragment = "the field * does not exists within the record Trade"; assertErrorMessageContains(msgFragment, e); } } /** * Creates a new HQL query message for use within the tests. * * @param database the database name * @param query the HQL query * @return a new HQL query message for use within the tests. */ private static Msg<HqlQueryPayload> newMsg(String database, String query) throws IOException { HqlQueryPayload payload = new HqlQueryPayload(database, query); return Msg.newRequestMsg(OpCode.HQL_QUERY, payload); } /** * Test that the parsing of a select statement with the specified predicate works. * * @param predicate the predicate to test * @throws HorizonDBException if a problem occurs */ private void testParseSelectWithPredicate(String predicate) throws HorizonDBException, IOException { createDatabaseAndTimeSeries(); Msg<SelectPayload> msg = QueryParser.parse(this.configuration, this.databaseManager, newMsg("test", "SELECT * FROM Dax WHERE " + predicate + ";")); assertEquals("Dax", msg.getPayload().getSeriesName()); assertEquals(predicate, msg.getPayload().getPredicate().toString()); } /** * Creates a new configuration instance. * @return a new configuration instance. */ private static Configuration newConfiguration() { return Configuration.newBuilder().build(); } /** * Creates a new database with a time series. */ private void createDatabaseAndTimeSeries() throws IOException, HorizonDBException { createDatabase(); Database database = this.databaseManager.getDatabase("test"); database.createTimeSeries(getTimeSeriesDefinition(), true); } /** * Creates a new database * @throws IOException if an I/O problem occurs while creating the database. * @throws HorizonDBException if the database cannot be created */ private void createDatabase() throws IOException, HorizonDBException { this.databaseManager.createDatabase(new DatabaseDefinition("test"), true); } /** * Creates a new <code>DatabaseManager</code> stub. * @param configuration the database configuration * @return a new <code>DatabaseManager</code> stub. */ private static DatabaseManager newDatabaseManager(Configuration configuration) { TimeSeriesPartitionManager partitionManager = new InMemoryTimeSeriesPartitionManager(configuration); TimeSeriesManager timeSeriesManager = new InMemoryTimeSeriesManager(partitionManager, configuration); return new InMemoryDatabaseManager(configuration, timeSeriesManager); } /** * Returns the definition of the time series used within the tests * @return the definition of the time series used by within tests */ private static TimeSeriesDefinition getTimeSeriesDefinition() { RecordTypeDefinition quote = RecordTypeDefinition.newBuilder("Quote") .addDecimalField("bidPrice") .addDecimalField("askPrice") .addIntegerField("bidVolume") .addIntegerField("askVolume") .build(); RecordTypeDefinition trade = RecordTypeDefinition.newBuilder("Trade") .addDecimalField("price") .addIntegerField("volume") .build(); TimeSeriesDefinition expected = TimeSeriesDefinition.newBuilder("Dax") .addRecordType(quote) .addRecordType(trade) .timeUnit(TimeUnit.NANOSECONDS) .timeZone(TimeZone.getTimeZone("Europe/Berlin")) .build(); return expected; } }