/*
 * 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;
  }
}
