/*
 * 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.phoenix.end2end;

import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.util.PropertiesUtil;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.apache.phoenix.thirdparty.com.google.common.primitives.Doubles;
import org.apache.phoenix.thirdparty.com.google.common.primitives.Floats;


@Category(ParallelStatsDisabledTest.class)
public class ArithmeticQueryIT extends ParallelStatsDisabledIT {

    @Test
    public void testDecimalUpsertValue() throws Exception {
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        conn.setAutoCommit(false);
        try {
            String testDecimalArithmetic = generateUniqueName();
            String ddl = "CREATE TABLE " + testDecimalArithmetic +
                    "  (pk VARCHAR NOT NULL PRIMARY KEY, " +
                    "col1 DECIMAL(31,0), col2 DECIMAL(5), col3 DECIMAL(5,2), col4 DECIMAL)";
            createTestTable(getUrl(), ddl);
            
            // Test upsert correct values 
            String query = "UPSERT INTO " + testDecimalArithmetic
                + "(pk, col1, col2, col3, col4) VALUES(?,?,?,?,?)";
            PreparedStatement stmt = conn.prepareStatement(query);
            stmt.setString(1, "valueOne");
            stmt.setBigDecimal(2, new BigDecimal("123456789123456789"));
            stmt.setBigDecimal(3, new BigDecimal("12345"));
            stmt.setBigDecimal(4, new BigDecimal("12.34"));
            stmt.setBigDecimal(5, new BigDecimal("12345.6789"));
            stmt.execute();
            conn.commit();
            
            query = "SELECT col1, col2, col3, col4 FROM " + testDecimalArithmetic
                + " WHERE pk = 'valueOne'";
            stmt = conn.prepareStatement(query);
            ResultSet rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertEquals(new BigDecimal("123456789123456789"), rs.getBigDecimal(1));
            assertEquals(new BigDecimal("12345"), rs.getBigDecimal(2));
            assertEquals(new BigDecimal("12.34"), rs.getBigDecimal(3));
            assertEquals(new BigDecimal("12345.6789"), rs.getBigDecimal(4));
            assertFalse(rs.next());
            
            query =
                "UPSERT INTO " + testDecimalArithmetic + "(pk, col1, col2, col3) VALUES(?,?,?,?)";
            stmt = conn.prepareStatement(query);
            stmt.setString(1, "valueTwo");
            stmt.setBigDecimal(2, new BigDecimal("1234567890123456789012345678901.12345"));
            stmt.setBigDecimal(3, new BigDecimal("12345.6789"));
            stmt.setBigDecimal(4, new BigDecimal("123.45678"));
            stmt.execute();
            conn.commit();
            
            query =
                "SELECT col1, col2, col3 FROM " + testDecimalArithmetic + " WHERE pk = 'valueTwo'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertEquals(new BigDecimal("1234567890123456789012345678901"), rs.getBigDecimal(1));
            assertEquals(new BigDecimal("12345"), rs.getBigDecimal(2));
            assertEquals(new BigDecimal("123.45"), rs.getBigDecimal(3));
            assertFalse(rs.next());
            
            // Test upsert incorrect values and confirm exceptions would be thrown.
            try {
                query = "UPSERT INTO " + testDecimalArithmetic
                    + "(pk, col1, col2, col3) VALUES(?,?,?,?)";
                stmt = conn.prepareStatement(query);
                stmt.setString(1, "badValues");
                // one more than max_precision
                stmt.setBigDecimal(2, new BigDecimal("12345678901234567890123456789012"));
                stmt.setBigDecimal(3, new BigDecimal("12345")); 
                stmt.setBigDecimal(4, new BigDecimal("123.45"));
                stmt.execute();
                conn.commit();
                fail("Should have caught bad values.");
            } catch (SQLException e) {
                assertEquals(SQLExceptionCode.DATA_EXCEEDS_MAX_CAPACITY.getErrorCode(),e.getErrorCode());
            }
            try {
                query = "UPSERT INTO " + testDecimalArithmetic
                    + "(pk, col1, col2, col3) VALUES(?,?,?,?)";
                stmt = conn.prepareStatement(query);
                stmt.setString(1, "badValues");
                stmt.setBigDecimal(2, new BigDecimal("123456"));
                // Exceeds specified precision by 1
                stmt.setBigDecimal(3, new BigDecimal("123456"));
                stmt.setBigDecimal(4, new BigDecimal("123.45"));
                stmt.execute();
                conn.commit();
                fail("Should have caught bad values.");
            } catch (SQLException e) {
                assertEquals(SQLExceptionCode.DATA_EXCEEDS_MAX_CAPACITY.getErrorCode(),e.getErrorCode());
            }
        } finally {
            conn.close();
        }
    }

    @Test
    public void testDecimalUpsertSelect() throws Exception {
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        conn.setAutoCommit(false);
        try {
            String source = generateUniqueName();
            String ddl = "CREATE TABLE " + source +
                    " (pk VARCHAR NOT NULL PRIMARY KEY, col1 DECIMAL(5,2), col2 DECIMAL(5,1), col3 DECIMAL(5,2), col4 DECIMAL(4,4))";
            createTestTable(getUrl(), ddl);
            String target = generateUniqueName();
            ddl = "CREATE TABLE " + target +
                    " (pk VARCHAR NOT NULL PRIMARY KEY, col1 DECIMAL(5,1), col2 DECIMAL(5,2), col3 DECIMAL(4,4))";
            createTestTable(getUrl(), ddl);
            
            String query = "UPSERT INTO " + source + "(pk, col1) VALUES(?,?)";
            PreparedStatement stmt = conn.prepareStatement(query);
            stmt.setString(1, "1");
            stmt.setBigDecimal(2, new BigDecimal("100.12"));
            stmt.execute();
            conn.commit();
            stmt.setString(1, "2");
            stmt.setBigDecimal(2, new BigDecimal("100.34"));
            stmt.execute();
            conn.commit();
            
            // Evaluated on client side.
            // source and target in different tables, values scheme compatible.
            query = "UPSERT INTO " + target + "(pk, col2) SELECT pk, col1 from " + source;
            stmt = conn.prepareStatement(query);
            stmt.execute();
            conn.commit();
            query = "SELECT col2 FROM " + target;
            stmt = conn.prepareStatement(query);
            ResultSet rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.12"), rs.getBigDecimal(1));
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.34"), rs.getBigDecimal(1));
            assertFalse(rs.next());
            // source and target in different tables, values requires scale chopping.
            query = "UPSERT INTO " + target + "(pk, col1) SELECT pk, col1 from " + source;
            stmt = conn.prepareStatement(query);
            stmt.execute();
            conn.commit();
            query = "SELECT col1 FROM " + target;
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.1"), rs.getBigDecimal(1));
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.3"), rs.getBigDecimal(1));
            assertFalse(rs.next());
            // source and target in different tables, values scheme incompatible.
            try {
                query = "UPSERT INTO " + target + "(pk, col3) SELECT pk, col1 from " + source;
                stmt = conn.prepareStatement(query);
                stmt.execute();
                conn.commit();
                fail("Should have caught bad upsert.");
            } catch (SQLException e) {
                assertEquals(SQLExceptionCode.DATA_EXCEEDS_MAX_CAPACITY.getErrorCode(),e.getErrorCode());
            }
            
            // Evaluate on server side.
            conn.setAutoCommit(true);
            // source and target in same table, values scheme compatible.
            query = "UPSERT INTO " + source + "(pk, col3) SELECT pk, col1 from " + source;
            stmt = conn.prepareStatement(query);
            stmt.execute();
            conn.commit();
            query = "SELECT col3 FROM " + source;
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.12"), rs.getBigDecimal(1));
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.34"), rs.getBigDecimal(1));
            assertFalse(rs.next());
            // source and target in same table, values requires scale chopping.
            query = "UPSERT INTO " + source + "(pk, col2) SELECT pk, col1 from " + source;
            stmt = conn.prepareStatement(query);
            stmt.execute();
            conn.commit();
            query = "SELECT col2 FROM " + source;
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.1"), rs.getBigDecimal(1));
            assertTrue(rs.next());
            assertEquals(new BigDecimal("100.3"), rs.getBigDecimal(1));
            assertFalse(rs.next());
            // source and target in same table, values scheme incompatible. should throw
            query = "UPSERT INTO " + source + "(pk, col4) SELECT pk, col1 from " + source;
            stmt = conn.prepareStatement(query);
            try {
                stmt.execute();
                conn.commit();
                fail();
            } catch (SQLException e) {
                assertEquals(SQLExceptionCode.DATA_EXCEEDS_MAX_CAPACITY.getErrorCode(), e.getErrorCode());
            }
            query = "SELECT col4 FROM " + source;
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            assertNull(rs.getBigDecimal(1));
            assertTrue(rs.next());
            assertNull(rs.getBigDecimal(1));
            assertFalse(rs.next());
        } finally {
            conn.close();
        }
    }

    @Test
    public void testDecimalAveraging() throws Exception {
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        conn.setAutoCommit(false);
        try {
            String testDecimalArithmetic = generateUniqueName();
            String ddl = "CREATE TABLE " + testDecimalArithmetic +
                    "  (pk VARCHAR NOT NULL PRIMARY KEY, col1 DECIMAL(31, 11), col2 DECIMAL(31,1), col3 DECIMAL(38,1))";
            createTestTable(getUrl(), ddl);
            
            String query =
                "UPSERT INTO " + testDecimalArithmetic + "(pk, col1, col2, col3) VALUES(?,?,?,?)";
            PreparedStatement stmt = conn.prepareStatement(query);
            stmt.setString(1, "1");
            stmt.setBigDecimal(2, new BigDecimal("99999999999999999999.1"));
            stmt.setBigDecimal(3, new BigDecimal("99999999999999999999.1"));
            stmt.setBigDecimal(4, new BigDecimal("9999999999999999999999999999999999999.1"));
            stmt.execute();
            conn.commit();
            stmt.setString(1, "2");
            stmt.setBigDecimal(2, new BigDecimal("0"));
            stmt.setBigDecimal(3, new BigDecimal("0"));
            stmt.setBigDecimal(4, new BigDecimal("0"));
            stmt.execute();
            conn.commit();
            stmt.setString(1, "3");
            stmt.setBigDecimal(2, new BigDecimal("0"));
            stmt.setBigDecimal(3, new BigDecimal("0"));
            stmt.setBigDecimal(4, new BigDecimal("0"));
            stmt.execute();
            conn.commit();
            
            // Averaging
            // result scale should be: max(max(ls, rs), 4).
            // We are not imposing restriction on precisioin.
            query = "SELECT avg(col1) FROM " + testDecimalArithmetic;
            stmt = conn.prepareStatement(query);
            ResultSet rs = stmt.executeQuery();
            assertTrue(rs.next());
            BigDecimal result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("33333333333333333333.03333333333"), result);
            
            query = "SELECT avg(col2) FROM " + testDecimalArithmetic;
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("33333333333333333333.0333"), result);
            
            // We cap our decimal to a precision of 38.
            query = "SELECT avg(col3) FROM " + testDecimalArithmetic;
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("3333333333333333333333333333333333333"), result);
        } finally {
            conn.close();
        }
    }

    @Test
    public void testRandomFunction() throws Exception {
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        conn.setAutoCommit(false);
        try {
            String testRandomFunction = generateUniqueName();
            String ddl =
                "CREATE TABLE " + testRandomFunction + " (pk VARCHAR NOT NULL PRIMARY KEY)";
            createTestTable(getUrl(), ddl);
            conn.createStatement().execute("upsert into " + testRandomFunction + " values ('x')");
            conn.createStatement().execute("upsert into " + testRandomFunction + " values ('y')");
            conn.createStatement().execute("upsert into " + testRandomFunction + " values ('z')");
            conn.commit();

            ResultSet rs = conn.createStatement().executeQuery(
                "select rand(), rand(), rand(1), rand(2), rand(1) from " + testRandomFunction);
            assertTrue(rs.next());
            double rand0 = rs.getDouble(1);
            double rand1 = rs.getDouble(3);
            double rand2 = rs.getDouble(4);
            assertTrue(rs.getDouble(1) != rs.getDouble(2));
            assertTrue(rs.getDouble(2) != rs.getDouble(3));
            assertTrue(rs.getDouble(3) == rs.getDouble(5));
            assertTrue(rs.getDouble(4) != rs.getDouble(5));
            assertTrue(rs.next());
            assertTrue(rand0 != rs.getDouble(1));
            assertTrue(rand1 != rs.getDouble(3));
            assertTrue(rand2 != rs.getDouble(4));
            double rand01 = rs.getDouble(1);
            double rand11 = rs.getDouble(3);
            double rand21 = rs.getDouble(4);
            assertTrue(rs.getDouble(1) != rs.getDouble(2));
            assertTrue(rs.getDouble(2) != rs.getDouble(3));
            assertTrue(rs.getDouble(3) == rs.getDouble(5));
            assertTrue(rs.getDouble(4) != rs.getDouble(5));
            assertTrue(rs.next());
            assertTrue(rand01 != rs.getDouble(1));
            assertTrue(rand11 != rs.getDouble(3));
            assertTrue(rand21 != rs.getDouble(4));
            assertTrue(rs.getDouble(1) != rs.getDouble(2));
            assertTrue(rs.getDouble(2) != rs.getDouble(3));
            assertTrue(rs.getDouble(3) == rs.getDouble(5));
            assertTrue(rs.getDouble(4) != rs.getDouble(5));
            double rand12 = rs.getDouble(3);

            rs = conn.createStatement().executeQuery(
                "select rand(), rand(), rand(1), rand(2), rand(1) from " + testRandomFunction);
            assertTrue(rs.next());
            assertTrue(rs.getDouble(1) != rs.getDouble(2));
            assertTrue(rs.getDouble(2) != rs.getDouble(3));
            assertTrue(rs.getDouble(3) == rs.getDouble(5));
            assertTrue(rs.getDouble(4) != rs.getDouble(5));
            assertTrue(rand0 != rs.getDouble(1));
            assertTrue(rand1 == rs.getDouble(3));
            assertTrue(rand2 == rs.getDouble(4));
            assertTrue(rs.next());
            assertTrue(rand01 != rs.getDouble(1));
            assertTrue(rand11 == rs.getDouble(3));
            assertTrue(rand21 == rs.getDouble(4));
            assertTrue(rs.next());
            assertTrue(rand12 == rs.getDouble(3));

            String testRandomFunction1 = generateUniqueName();
            ddl = "CREATE TABLE " + testRandomFunction1
                + " (pk VARCHAR NOT NULL PRIMARY KEY, v1 UNSIGNED_DOUBLE)";
            createTestTable(getUrl(), ddl);
            conn.createStatement().execute(
                "upsert into " + testRandomFunction1 + " select pk, rand(1) from " + testRandomFunction);
            conn.commit();

            rs = conn.createStatement().executeQuery(
                "select count(*) from " + testRandomFunction1 + " where v1 = rand(1)");
            assertTrue(rs.next());
            assertEquals(3, rs.getInt(1));

            rs = conn.createStatement().executeQuery(
                "select count(*) from " + testRandomFunction1 + " where v1 = rand(2)");
            assertTrue(rs.next());
            assertEquals(0, rs.getInt(1));

            conn.createStatement().execute(
                "delete from " + testRandomFunction1 + " where v1 = rand(2)");
            conn.commit();

            rs = conn.createStatement().executeQuery(
                "select count(*) from " + testRandomFunction1 + " where v1 = rand(1)");
            assertTrue(rs.next());
            assertEquals(3, rs.getInt(1));

            conn.setAutoCommit(true);
            conn.createStatement().execute("upsert into " + testRandomFunction1
                + " select pk, rand(2) from " + testRandomFunction1);

            rs = conn.createStatement().executeQuery(
                "select count(*) from " + testRandomFunction1 + " where v1 = rand(1)");
            assertTrue(rs.next());
            assertEquals(0, rs.getInt(1));

            rs = conn.createStatement().executeQuery(
                "select count(*) from " + testRandomFunction1 + " where v1 = rand(2)");
            assertTrue(rs.next());
            assertEquals(3, rs.getInt(1));

            conn.createStatement().execute(
                "delete from " + testRandomFunction1 + " where v1 = rand(2)");

            rs = conn.createStatement().executeQuery(
                "select count(*) from " + testRandomFunction1 + " where v1 = rand(2)");
            assertTrue(rs.next());
            assertEquals(0, rs.getInt(1));
        } finally {
            conn.close();
        }
    }

    @Test
    public void testDecimalArithmeticWithIntAndLong() throws Exception {
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        conn.setAutoCommit(false);
        try {
            String testDecimalArithmetic = generateUniqueName();
            String ddl = "CREATE TABLE " + testDecimalArithmetic +
                    "  (pk VARCHAR NOT NULL PRIMARY KEY, " +
                    "col1 DECIMAL(38,0), col2 DECIMAL(5, 2), col3 INTEGER, col4 BIGINT, col5 DECIMAL)";
            createTestTable(getUrl(), ddl);
            
            String query = "UPSERT INTO " + testDecimalArithmetic
                + "(pk, col1, col2, col3, col4, col5) VALUES(?,?,?,?,?,?)";
            PreparedStatement stmt = conn.prepareStatement(query);
            stmt.setString(1, "testValueOne");
            stmt.setBigDecimal(2, new BigDecimal("1234567890123456789012345678901"));
            stmt.setBigDecimal(3, new BigDecimal("123.45"));
            stmt.setInt(4, 10);
            stmt.setLong(5, 10L);
            stmt.setBigDecimal(6, new BigDecimal("111.111"));
            stmt.execute();
            conn.commit();

            stmt.setString(1, "testValueTwo");
            stmt.setBigDecimal(2, new BigDecimal("12345678901234567890123456789012345678"));
            stmt.setBigDecimal(3, new BigDecimal("123.45"));
            stmt.setInt(4, 10);
            stmt.setLong(5, 10L);
            stmt.setBigDecimal(6, new BigDecimal("123456789.0123456789"));
            stmt.execute();
            conn.commit();
            
            // INT has a default precision and scale of (10, 0)
            // LONG has a default precision and scale of (19, 0)
            query = "SELECT col1 + col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            ResultSet rs = stmt.executeQuery();
            assertTrue(rs.next());
            BigDecimal result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1234567890123456789012345678911"), result);
            
            query = "SELECT col1 + col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1234567890123456789012345678911"), result);
            
            query = "SELECT col2 + col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("133.45"), result);
            
            query = "SELECT col2 + col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("133.45"), result);
            
            query = "SELECT col5 + col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("121.111"), result);
            
            query = "SELECT col5 + col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("121.111"), result);
            
            query = "SELECT col1 - col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1234567890123456789012345678891"), result);
            
            query = "SELECT col1 - col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1234567890123456789012345678891"), result);
            
            query = "SELECT col2 - col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("113.45"), result);
            
            query = "SELECT col2 - col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("113.45"), result);
            
            query = "SELECT col5 - col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("101.111"), result);
            
            query = "SELECT col5 - col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("101.111"), result);
            
            query = "SELECT col1 * col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1.234567890123456789012345678901E+31"), result);
            
            query = "SELECT col1 * col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1.234567890123456789012345678901E+31"), result);

            query = "SELECT col1 * col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1.234567890123456789012345678901E+31"), result);
            
            try {
            	query =
                  "SELECT col1 * col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueTwo'";
            	stmt = conn.prepareStatement(query);
            	rs = stmt.executeQuery();
            	assertTrue(rs.next());
            	result = rs.getBigDecimal(1);
            	fail("Should have caught error.");
            } catch (SQLException e) {
                assertEquals(SQLExceptionCode.DATA_EXCEEDS_MAX_CAPACITY.getErrorCode(),e.getErrorCode());
            }
            
            try {
            	query =
                  "SELECT col1 * col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueTwo'";
            	stmt = conn.prepareStatement(query);
            	rs = stmt.executeQuery();
            	assertTrue(rs.next());
            	result = rs.getBigDecimal(1);
            	fail("Should have caught error.");
            } catch (SQLException e) {
                assertEquals(SQLExceptionCode.DATA_EXCEEDS_MAX_CAPACITY.getErrorCode(),e.getErrorCode());
            }
            
            query = "SELECT col4 * col5 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(0, result.compareTo(new BigDecimal("1111.11")));

            query = "SELECT col3 * col5 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(0, result.compareTo(new BigDecimal("1111.11")));
            
            query = "SELECT col2 * col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1234.5"), result);
            
            // Result scale has value of 0
            query = "SELECT col1 / col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1.2345678901234567890123456789E+29"), result);
            
            query = "SELECT col1 / col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("1.2345678901234567890123456789E+29"), result);
            
            // Result scale is 2.
            query = "SELECT col2 / col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("12.34"), result);
            
            query = "SELECT col2 / col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("12.34"), result);
            
            // col5 has NO_SCALE, so the result's scale is not expected to be truncated to col5 value's scale of 4
            query = "SELECT col5 / col3 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("11.1111"), result);
            
            query = "SELECT col5 / col4 FROM " + testDecimalArithmetic + " WHERE pk='testValueOne'";
            stmt = conn.prepareStatement(query);
            rs = stmt.executeQuery();
            assertTrue(rs.next());
            result = rs.getBigDecimal(1);
            assertEquals(new BigDecimal("11.1111"), result);
        } finally {
            conn.close();
        }
    }
    @Test
    public void testSumDouble() throws Exception {
        String tableName = "TBL_" + generateUniqueName();
        initSumDoubleValues(tableName, null, getUrl());
        String query = "SELECT SUM(d) FROM " + tableName ;
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        try {
            PreparedStatement statement = conn.prepareStatement(query);
            ResultSet rs = statement.executeQuery();
            assertTrue (rs.next());
            assertTrue(Doubles.compare(rs.getDouble(1), 0.015)==0);
            assertFalse(rs.next());
        } finally {
            conn.close();
        }
    }
    
    @Test
    public void testSumUnsignedDouble() throws Exception {
        String tableName = "TBL_" + generateUniqueName();
        initSumDoubleValues(tableName, null, getUrl());
        String query = "SELECT SUM(ud) FROM " + tableName ;
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        try {
            PreparedStatement statement = conn.prepareStatement(query);
            ResultSet rs = statement.executeQuery();
            assertTrue (rs.next());
            assertTrue(Doubles.compare(rs.getDouble(1), 0.015)==0);
            assertFalse(rs.next());
        } finally {
            conn.close();
        }
    }
    
    @Test
    public void testSumFloat() throws Exception {
        String tableName = "TBL_" + generateUniqueName();
        initSumDoubleValues(tableName, null, getUrl());
        String query = "SELECT SUM(f) FROM " + tableName ;
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        try {
            PreparedStatement statement = conn.prepareStatement(query);
            ResultSet rs = statement.executeQuery();
            assertTrue (rs.next());
            assertTrue(Floats.compare(rs.getFloat(1), 0.15f)==0);
            assertFalse(rs.next());
        } finally {
            conn.close();
        }
    }
    
    @Test
    public void testSumUnsignedFloat() throws Exception {
        String tableName = "TBL_" + generateUniqueName();
        initSumDoubleValues(tableName, null, getUrl());
        String query = "SELECT SUM(uf) FROM " + tableName;
        Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
        Connection conn = DriverManager.getConnection(getUrl(), props);
        try {
            PreparedStatement statement = conn.prepareStatement(query);
            ResultSet rs = statement.executeQuery();
            assertTrue (rs.next());
            assertTrue(Floats.compare(rs.getFloat(1), 0.15f)==0);
            assertFalse(rs.next());
        } finally {
            conn.close();
        }
    }
    
    private String initIntegerTable(Connection conn) throws SQLException {
        String arithmetic_test = generateUniqueName();
        String ddl = "CREATE TABLE " + arithmetic_test
            + " (six INTEGER PRIMARY KEY, four INTEGER, three INTEGER)";
        conn.createStatement().execute(ddl);
        String dml = "UPSERT INTO " + arithmetic_test + " VALUES(6, 4, 3)";
        conn.createStatement().execute(dml);
        conn.commit();
        return arithmetic_test;
    }
    
    @Test
    public void testOrderOfOperationsAdditionSubtraction() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 + 4 - 3
        // 10 - 3
        // 7
        rs = conn.createStatement().executeQuery("SELECT six + four - three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(7, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 - 3 + 6
        // 1 + 6
        // 7
        rs = conn.createStatement().executeQuery("SELECT four - three + six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(7, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsAdditionMultiplication() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 + 4 * 3
        // 6 + 12
        // 18
        rs = conn.createStatement().executeQuery("SELECT six + four * three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(18, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 * 3 + 6
        // 12 * 6     
        // 18
        rs = conn.createStatement().executeQuery("SELECT four * three + six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(18, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsAdditionDivision() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 + 4 / 3
        // 6 + 1
        // 7
        rs = conn.createStatement().executeQuery("SELECT six + four / three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(7, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 / 3 + 6
        // 1 + 6     
        // 7
        rs = conn.createStatement().executeQuery("SELECT four / three + six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(7, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsAdditionModulus() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 + 4 % 3
        // 6 + 1
        // 7
        rs = conn.createStatement().executeQuery("SELECT six + four % three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(7, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 % 3 + 6
        // 1 + 6
        // 7
        rs = conn.createStatement().executeQuery("SELECT four % three + six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(7, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsSubtrationMultiplication() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 - 4 * 3
        // 6 - 12
        // -6
        rs = conn.createStatement().executeQuery("SELECT six - four * three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(-6, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 * 3 - 6
        // 12 - 6     
        // 6
        rs = conn.createStatement().executeQuery("SELECT four * three - six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(6, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsSubtractionDivision() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 - 4 / 3
        // 6 - 1     (integer division)
        // 5
        rs = conn.createStatement().executeQuery("SELECT six - four / three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(5, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 / 3 - 6
        // 1 - 6     (integer division)
        // -5
        rs = conn.createStatement().executeQuery("SELECT four / three - six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(-5, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsSubtractionModulus() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 - 4 % 3
        // 6 - 1
        // 5
        rs = conn.createStatement().executeQuery("SELECT six - four % three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(5, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 % 3 - 6
        // 1 - 6
        // -5
        rs = conn.createStatement().executeQuery("SELECT four % three - six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(-5, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsMultiplicationDivision() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 * 4 / 3
        // 24 / 3
        // 8
        rs = conn.createStatement().executeQuery("SELECT six * four / three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(8, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 / 3 * 6
        // 1 * 6     (integer division)
        // 6
        rs = conn.createStatement().executeQuery("SELECT four / three * six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(6, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsMultiplicationModulus() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 * 4 % 3
        // 24 % 3
        // 0
        rs = conn.createStatement().executeQuery("SELECT six * four % three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(0, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 % 3 * 6
        // 1 * 6
        // 6
        rs = conn.createStatement().executeQuery("SELECT four % three * six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(6, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testOrderOfOperationsDivisionModulus() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String tableName = initIntegerTable(conn);
        ResultSet rs;
        
        // 6 / 4 % 3
        // 1 % 3     (integer division)
        // 1
        rs = conn.createStatement().executeQuery("SELECT six / four % three FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(1, rs.getLong(1));
        assertFalse(rs.next());
        
        // 4 % 3 / 6
        // 1 / 6
        // 0         (integer division)
        rs = conn.createStatement().executeQuery("SELECT four % three / six FROM " + tableName);
        assertTrue(rs.next());
        assertEquals(0, rs.getLong(1));
        assertFalse(rs.next());
    }
    
    @Test
    public void testCastingOnConstantAddInArithmeticEvaluation() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String testTable = generateUniqueName();
        String ddl = "CREATE TABLE IF NOT EXISTS " + testTable
            + " (k1 INTEGER NOT NULL, v1 INTEGER CONSTRAINT pk PRIMARY KEY (k1))";
        conn.createStatement().execute(ddl);
        String dml = "UPSERT INTO " + testTable + " (k1, v1) VALUES (2, 2)";
        conn.createStatement().execute(dml);
        conn.commit();

        ResultSet rs = conn.createStatement().executeQuery(
            "SELECT k1 / (v1 + 0.5) FROM " + testTable);
        assertTrue(rs.next());
        double d = rs.getDouble(1);
        assertEquals(0.8, d, 0.01);
    }

    @Test
    public void testCastingOnConstantSubInArithmeticEvaluation() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String testTable = generateUniqueName();
        String ddl = "CREATE TABLE IF NOT EXISTS " + testTable
            + " (k1 INTEGER NOT NULL, v1 INTEGER CONSTRAINT pk PRIMARY KEY (k1))";
        conn.createStatement().execute(ddl);
        String dml = "UPSERT INTO " + testTable + " (k1, v1) VALUES (2, 2)";
        conn.createStatement().execute(dml);
        conn.commit();

        ResultSet rs = conn.createStatement().executeQuery(
            "SELECT k1 / (v1 - 0.5) FROM " + testTable);
        assertTrue(rs.next());
        assertEquals(1.333333333, rs.getDouble(1), 0.001);
    }

    @Test
    public void testFloatingPointUpsert() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String test = generateUniqueName();
        String ddl =
            "CREATE TABLE " + test + " (id VARCHAR not null primary key, name VARCHAR, lat FLOAT)";
        conn.createStatement().execute(ddl);
        String dml = "UPSERT INTO " + test + "(id,name,lat) VALUES ('testid', 'testname', -1.00)";
        conn.createStatement().execute(dml);
        conn.commit();

        ResultSet rs = conn.createStatement().executeQuery("SELECT lat FROM " + test);
        assertTrue(rs.next());
        assertEquals(-1.0f, rs.getFloat(1), 0.001);
    }

    @Test
    public void testFloatingPointMultiplicationUpsert() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String test = generateUniqueName();
        String ddl =
            "CREATE TABLE " + test + " (id VARCHAR not null primary key, name VARCHAR, lat FLOAT)";
        conn.createStatement().execute(ddl);
        String dml =
            "UPSERT INTO " + test + "(id,name,lat) VALUES ('testid', 'testname', -1.00 * 1)";
        conn.createStatement().execute(dml);
        conn.commit();

        ResultSet rs = conn.createStatement().executeQuery("SELECT lat FROM " + test);
        assertTrue(rs.next());
        assertEquals(-1.0f, rs.getFloat(1), 0.001);
    }
    
    @Test
    public void testSystemTableHasDoubleForExponentialNumber() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl());
        String test = generateUniqueName();
        String ddl = "CREATE TABLE " + test + " (id VARCHAR not null primary key, num FLOAT)";
        conn.createStatement().execute(ddl);
        String dml = "UPSERT INTO " + test + "(id,num) VALUES ('testid', 1.2E3)";
        conn.createStatement().execute(dml);
        conn.commit();

        ResultSet rs = conn.createStatement().executeQuery("SELECT 1.2E3 FROM \"SYSTEM\".\"CATALOG\" LIMIT 1");
        assertTrue(rs.next());
        assertTrue(rs.getObject(1) instanceof Double);
    }
    
    @Test
    public void testFloatingPointWithExponentialNotation() throws Exception {
    	Float[] expected = {1.5E7f, 1.5E-7f, -1.5E-7f, 12E-5f, -.12E+34f};
    	String[] values = {"1.5e7", "1.5e-7", "-1.5e-7", "12E-5", "-.12E+34"};
        ResultSet rs = createTableWithValues(values, "FLOAT");
        for (int i = 0; i < expected.length; i++) {
        	assertEquals(expected[i], rs.getFloat(i+1), 0.001);
        }
    }
    
    @Test
    public void testDoubleWithExponentialNotation() throws Exception {
    	Double[] expected = {1.5E7d, 1.5E-7d, -1.5E-7d, 12E-5d, -.654E-321d, .1234E+56d};
    	String[] values = {"1.5e7", "1.5e-7", "-1.5e-7", "12E-5", "-.654E-321", ".1234E+56"};
    	ResultSet rs = createTableWithValues(values, "DOUBLE");
        for (int i = 0; i < expected.length; i++) {
        	assertEquals(expected[i], rs.getDouble(i+1), 0.001);
        }
    }
    
    private ResultSet createTableWithValues(String[] values, String valueType) throws SQLException {
    	Connection conn = DriverManager.getConnection(getUrl());
        String test = generateUniqueName();
        StringBuilder ddl = new StringBuilder(
            "CREATE TABLE " + test + " (id VARCHAR not null primary key");
        StringBuilder dmll = new StringBuilder("UPSERT INTO " + test + "(id,");
        StringBuilder dmlr = new StringBuilder(") VALUES ('testid'");
        StringBuilder select = new StringBuilder("SELECT");
        for(int i = 0; i < values.length; i++) {
        	ddl.append(", num").append(i).append(" ").append(valueType);
        	dmll.append("num").append(i).append(",");
        	dmlr.append(", ").append(values[i]);
        	select.append(" num").append(i).append(",");
        }
        ddl.append(")");
        dmlr.append(")");
        dmll.deleteCharAt(dmll.length()-1);
        select.deleteCharAt(select.length()-1);
        select.append(" FROM " + test);
        conn.createStatement().execute(ddl.toString());
        conn.createStatement().execute(dmll.toString() + dmlr.toString());
        conn.commit();

        ResultSet rs = conn.createStatement().executeQuery(select.toString());
        rs.next();
        return rs;
    }
}