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

import static org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants.MAX_QUALIFIER;
import static org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants.MIN_QUALIFIER;
import static org.apache.phoenix.query.QueryConstants.ENCODED_CQ_COUNTER_INITIAL_VALUE;
import static org.apache.phoenix.query.QueryConstants.ENCODED_EMPTY_COLUMN_NAME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.sql.Array;
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
import org.apache.phoenix.jdbc.PhoenixResultSet;
import org.apache.phoenix.jdbc.PhoenixStatement;
import org.apache.phoenix.parse.DeleteStatement;
import org.apache.phoenix.parse.HintNode.Hint;
import org.apache.phoenix.parse.SQLParser;
import org.apache.phoenix.query.BaseConnectionlessQueryTest;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.util.PhoenixRuntime;
import org.apache.phoenix.util.PropertiesUtil;
import org.apache.phoenix.util.QueryUtil;
import org.apache.phoenix.util.SchemaUtil;
import org.apache.phoenix.util.TestUtil;
import org.junit.Ignore;
import org.junit.Test;

import org.apache.phoenix.thirdparty.com.google.common.base.Joiner;
import org.apache.phoenix.thirdparty.com.google.common.base.Splitter;

public class QueryOptimizerTest extends BaseConnectionlessQueryTest {

  public static final String SCHEMA_NAME = "";
  public static final String DATA_TABLE_FULL_NAME = SchemaUtil.getTableName(SCHEMA_NAME, "T");

  public QueryOptimizerTest() {
  }

  @Test
  public void testRVCUsingPkColsReturnedByPlanShouldUseIndex() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE T (k VARCHAR NOT NULL PRIMARY KEY, v1 CHAR(15), v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX IDX ON T(v1, v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    String query = "select * from t where (v1, v2, k) > ('1', '2', '3')";
    QueryPlan plan = stmt.optimizeQuery(query);
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testOrderByOptimizedOut() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE foo (k VARCHAR NOT NULL PRIMARY KEY, v VARCHAR) IMMUTABLE_ROWS=true");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT * FROM foo ORDER BY k");
    assertEquals(OrderBy.FWD_ROW_KEY_ORDER_BY, plan.getOrderBy());
  }

  @Test
  public void testOrderByDropped() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    try {
      conn.createStatement().execute(
        "CREATE TABLE foo (k VARCHAR NOT NULL PRIMARY KEY, v VARCHAR) IMMUTABLE_ROWS=true");
      PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
      QueryPlan plan = stmt.optimizeQuery("SELECT * FROM foo ORDER BY 'a','b','c'");
      assertTrue(plan.getOrderBy().getOrderByExpressions().isEmpty());
    } finally {
      conn.close();
    }
  }

  @Test
  public void testOrderByNotDropped() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE foo (k VARCHAR NOT NULL PRIMARY KEY, v VARCHAR) IMMUTABLE_ROWS=true");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT * FROM foo ORDER BY v");
    assertFalse(plan.getOrderBy().getOrderByExpressions().isEmpty());
  }

  @Test
  public void testOrderByDroppedCompositeKey() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE foo (j INTEGER NOT NULL, k BIGINT NOT NULL, v VARCHAR CONSTRAINT pk PRIMARY KEY (j,k)) IMMUTABLE_ROWS=true");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT * FROM foo ORDER BY j,k");
    assertEquals(OrderBy.FWD_ROW_KEY_ORDER_BY, plan.getOrderBy());
  }

  @Test
  public void testOrderByNotDroppedCompositeKey() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE foo (j INTEGER NOT NULL, k BIGINT NOT NULL, v VARCHAR CONSTRAINT pk PRIMARY KEY (j,k)) IMMUTABLE_ROWS=true");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT * FROM foo ORDER BY k,j");
    assertFalse(plan.getOrderBy().getOrderByExpressions().isEmpty());
  }

  @Test
  public void testChooseIndexOverTable() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE v1 = 'bar'");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseTableOverIndex() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT v1 FROM t WHERE k = 1");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseTableForSelection() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT v1,v2 FROM t WHERE v1 = 'bar'");
    // Choose T because v2 is not in index
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseTableForDynCols() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t(v3 VARCHAR) WHERE v1 = 'bar'");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseTableForSelectionStar() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT * FROM t WHERE v1 = 'bar'");
    // Choose T because v2 is not in index
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexEvenWithSelectionStar() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1) INCLUDE (v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT * FROM t WHERE v1 = 'bar'");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromOrderBy() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE k > 30 ORDER BY v1 LIMIT 5");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChoosePointLookupOverOrderByRemoval() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE k = 30 ORDER BY v1 LIMIT 5"); // Prefer
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromOrderByDesc() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY DESC, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE k > 30 ORDER BY v1, k DESC LIMIT 5");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseTableFromOrderByAsc() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY DESC, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE k > 30 ORDER BY v1, k LIMIT 5");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromOrderByAsc() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY DESC, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1, k)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE k > 30 ORDER BY v1, k LIMIT 5");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChoosePointLookupOverOrderByDesc() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY DESC, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1, k)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE k = 30 ORDER BY v1, k LIMIT 5");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexWithLongestRowKey() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx1 ON t(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX idx2 ON t(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT k FROM t WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX2", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testIgnoreIndexesBasedOnHint() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx1 ON t(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX idx2 ON t(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan =
      stmt.optimizeQuery("SELECT /*+NO_INDEX*/ k FROM t WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromHint() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx1 ON t(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX idx2 ON t(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan =
      stmt.optimizeQuery("SELECT /*+ INDEX(t  idx1) */ k FROM t WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX1", plan.getTableRef().getTable().getTableName().getString());
    plan = stmt.optimizeQuery("SELECT k FROM t WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX2", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromCaseSensitiveHint() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE \"t\" (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx1 ON \"t\"(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX idx2 ON \"t\"(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery(
      "SELECT /*+ INDEX(\"t\" idx1) */ k FROM \"t\" WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX1", plan.getTableRef().getTable().getTableName().getString());
    plan = stmt.optimizeQuery("SELECT k FROM \"t\" WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX2", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromCaseSensitiveHint2() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE \"t\" (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX \"idx1\" ON \"t\"(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX \"idx2\" ON \"t\"(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery(
      "SELECT /*+ INDEX(\"t\" \"idx1\") */ k FROM \"t\" WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("idx1", plan.getTableRef().getTable().getTableName().getString());
    plan = stmt.optimizeQuery("SELECT k FROM \"t\" WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("idx2", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testChooseIndexFromDoubleQuotedHint() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx1 ON t(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX idx2 ON t(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery(
      "SELECT /*+ INDEX(t  \"IDX1\") INDEX(t  idx3) */ k FROM t WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX1", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testIndexHintParsing() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx1 ON t(v1) INCLUDE(v2)");
    conn.createStatement().execute("CREATE INDEX idx2 ON t(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery(
      "SELECT /*+  INDEX(t  idx3 idx4 \"idx5\") INDEX(t idx6 idx1) */ k FROM t WHERE v1 = 'foo' AND v2 = 'bar'");
    assertEquals("IDX1", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testDataTableOverIndexHint() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1,v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan =
      stmt.optimizeQuery("SELECT /*+ " + Hint.USE_DATA_OVER_INDEX_TABLE + " */ * FROM t");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
    // unhinted still uses index
    plan = stmt.optimizeQuery("SELECT * FROM t");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
    // hinting with a WHERE clause still uses the index
    plan = stmt.optimizeQuery(
      "SELECT /*+ " + Hint.USE_DATA_OVER_INDEX_TABLE + " */ * FROM t WHERE v1 = 'foo'");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  // Tests that a DELETE without a WHERE clause uses the data table (for parallel deletion on server
  // side)
  // DELETE with a WHERE clause should use the index on the client side
  @Test
  public void testDelete() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1,v2)");
    conn.setAutoCommit(true);
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    SQLParser parser = new SQLParser("DELETE FROM t");
    DeleteStatement delete = (DeleteStatement) parser.parseStatement();
    DeleteCompiler compiler = new DeleteCompiler(stmt, null);
    MutationPlan plan = compiler.compile(delete);
    assertEquals("T", plan.getQueryPlan().getTableRef().getTable().getTableName().getString());
    assertEquals(plan.getClass(), DeleteCompiler.ServerSelectDeleteMutationPlan.class);
    parser = new SQLParser("DELETE FROM t WHERE v1 = 'foo'");
    delete = (DeleteStatement) parser.parseStatement();
    plan = compiler.compile(delete);
    assertEquals("IDX", plan.getQueryPlan().getTableRef().getTable().getTableName().getString());
    assertEquals(plan.getClass(), DeleteCompiler.ClientSelectDeleteMutationPlan.class);
  }

  @Test
  public void testChooseSmallerTable() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR) IMMUTABLE_ROWS=true");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT count(*) FROM t");
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testRVCForTableWithSecondaryIndexBasic() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE T (k VARCHAR NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX IDX ON T(v1, v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    String query = "select * from t where (v1, v2) <= ('1', '2')";
    QueryPlan plan = stmt.optimizeQuery(query);
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testRVCAllColsForTableWithSecondaryIndexBasic() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE T (k VARCHAR NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX IDX ON T(v1, v2)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    String query = "select * from t where (k, v1, v2) <= ('3', '1', '2')";
    QueryPlan plan = stmt.optimizeQuery(query);
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  // Multi-tenant = false; Query uses index = false; Salted = true
  public void testAssertQueryPlanDetails1() throws Exception {
    testAssertQueryPlanDetails(false, false, true);
  }

  @Test
  // Multi-tenant = true; Query uses index = false; Salted = true
  public void testAssertQueryPlanDetails2() throws Exception {
    testAssertQueryPlanDetails(true, false, true);
  }

  @Test
  // Multi-tenant = true; Query uses index = true; Salted = false
  public void testAssertQueryPlanDetails3() throws Exception {
    testAssertQueryPlanDetails(true, true, true);
  }

  @Test
  // Multi-tenant = false; Query uses index = true; Salted = true
  public void testAssertQueryPlanDetails4() throws Exception {
    testAssertQueryPlanDetails(false, true, true);
  }

  @Test
  // Multi-tenant = false; Query uses index = false; Salted = false
  public void testAssertQueryPlanDetails5() throws Exception {
    testAssertQueryPlanDetails(false, false, false);
  }

  @Test
  // Multi-tenant = true; Query uses index = false; Salted = false
  public void testAssertQueryPlanDetails6() throws Exception {
    testAssertQueryPlanDetails(true, false, false);
  }

  @Test
  // Multi-tenant = true; Query uses index = true; Salted = false
  public void testAssertQueryPlanDetails7() throws Exception {
    testAssertQueryPlanDetails(true, true, false);
  }

  @Test
  // Multi-tenant = false; Query uses index = true; Salted = false
  public void testAssertQueryPlanDetails8() throws Exception {
    testAssertQueryPlanDetails(false, true, false);
  }

  @Test
  public void testQueryOptimizerShouldSelectThePlanWithMoreNumberOfPKColumns() throws Exception {
    Connection conn1 = DriverManager.getConnection(getUrl());
    Connection conn2 = DriverManager.getConnection(getUrl());
    conn1.createStatement().execute(
      "create table index_test_table (a varchar not null,b varchar not null,c varchar not null,d varchar,e varchar, f varchar constraint pk primary key(a,b,c))");
    conn1.createStatement()
      .execute("create index INDEX_TEST_TABLE_INDEX_D on INDEX_TEST_TABLE(A,D) include(B,C,E,F)");
    conn1.createStatement()
      .execute("create index INDEX_TEST_TABLE_INDEX_F on INDEX_TEST_TABLE(A,F) include(B,C,D,E)");
    ResultSet rs = conn2.createStatement().executeQuery(
      "explain select * from INDEX_TEST_TABLE where A in ('1','2','3','4','5') and F in ('1111','2222','3333')");
    assertEquals(
      "CLIENT PARALLEL 1-WAY SKIP SCAN ON 15 KEYS OVER INDEX_TEST_TABLE_INDEX_F ['1','1111'] - ['5','3333']",
      QueryUtil.getExplainPlan(rs));
  }

  @Test
  public void testCharArrayLength() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE TEST.TEST (testInt INTEGER, testCharArray CHAR(3)[], testByteArray BINARY(7)[], "
        + "CONSTRAINT test_pk PRIMARY KEY(testInt)) DEFAULT_COLUMN_FAMILY='T'");
    conn.createStatement().execute(
      "CREATE INDEX TEST_INDEX ON TEST.TEST (testInt) INCLUDE (testCharArray, testByteArray)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);

    QueryPlan plan = stmt.optimizeQuery(
      "SELECT /*+ INDEX(TEST.TEST TEST_INDEX)*/ testCharArray,testByteArray FROM TEST.TEST");
    List<PColumn> columns = plan.getTableRef().getTable().getColumns();
    assertEquals(3, columns.size());
    assertEquals(3, columns.get(1).getMaxLength().intValue());
    assertEquals(7, columns.get(2).getMaxLength().intValue());
  }

  private void testAssertQueryPlanDetails(boolean multitenant, boolean useIndex, boolean salted)
    throws Exception {
    String sql;
    PreparedStatement stmt;
    Connection conn = DriverManager.getConnection(getUrl(), new Properties());
    try {
      // create table
      conn.createStatement()
        .execute("create table " + "XYZ.ABC" + "   (organization_id char(15) not null, \n"
          + "    \"DEC\" DECIMAL(10,2) not null,\n"
          + "    a_string_array varchar(100) array[] not null,\n" + "    b_string varchar(100),\n"
          + "    CF.a_integer integer,\n" + "    a_date date,\n"
          + "    CONSTRAINT pk PRIMARY KEY (organization_id, \"DEC\", a_string_array)\n" + ")"
          + (salted ? "SALT_BUCKETS=4" : "")
          + (multitenant == true ? (salted ? ",MULTI_TENANT=true" : "MULTI_TENANT=true") : ""));

      if (useIndex) {
        // create index
        conn.createStatement()
          .execute("CREATE INDEX ABC_IDX ON XYZ.ABC (CF.a_integer) INCLUDE (a_date)");
      }

      // switch to a tenant specific connection if multi-tenant.
      conn = multitenant ? DriverManager.getConnection(getUrl("tenantId")) : conn;

      // create a tenant specific view if multi-tenant
      if (multitenant) {
        conn.createStatement()
          .execute("CREATE VIEW ABC_VIEW (ORG_ID VARCHAR) AS SELECT * FROM XYZ.ABC");
      }

      String expectedColNames = multitenant
        ? addQuotes(null, "DEC,A_STRING_ARRAY")
        : addQuotes(null, "ORGANIZATION_ID,DEC,A_STRING_ARRAY");
      String expectedColumnNameDataTypes = multitenant
        ? "\"DEC\" DECIMAL(10,2),\"A_STRING_ARRAY\" VARCHAR(100) ARRAY"
        : "\"ORGANIZATION_ID\" CHAR(15),\"DEC\" DECIMAL(10,2),\"A_STRING_ARRAY\" VARCHAR(100) ARRAY";
      String tableName = multitenant ? "ABC_VIEW" : "XYZ.ABC";
      String tenantFilter = multitenant ? "" : "organization_id = ? AND ";
      String orderByRowKeyClause = multitenant ? "DEC" : "organization_id";

      // Filter on row key columns of data table. No order by. No limit.
      sql = "SELECT CF.a_integer FROM " + tableName + " where " + tenantFilter
        + " \"DEC\" = ? and a_string_array = ?";
      stmt = conn.prepareStatement(sql);
      int counter = 1;
      if (!multitenant) {
        stmt.setString(counter++, "ORGID");
      }
      stmt.setDouble(counter++, 1.23);
      String[] strArray = new String[2];
      strArray[0] = "AB";
      strArray[1] = "CD";
      Array array = conn.createArrayOf("VARCHAR", strArray);
      stmt.setArray(counter++, array);
      assertPlanDetails(stmt, expectedColNames, expectedColumnNameDataTypes, false, 0);

      counter = 1;
      // Filter on row key columns of data table. Order by row key columns. Limit specified.
      sql = "SELECT CF.a_integer FROM " + tableName + " where " + tenantFilter
        + " \"DEC\" = ? and a_string_array = ? ORDER BY " + orderByRowKeyClause + " LIMIT 100";
      stmt = conn.prepareStatement(sql);
      if (!multitenant) {
        stmt.setString(counter++, "ORGID");
      }
      stmt.setDouble(counter++, 1.23);
      array = conn.createArrayOf("VARCHAR", strArray);
      stmt.setArray(counter++, array);
      assertPlanDetails(stmt, expectedColNames, expectedColumnNameDataTypes, false, 100);

      counter = 1;
      // Filter on row key columns of data table. Order by non-row key columns. Limit specified.
      sql = "SELECT CF.a_integer FROM " + tableName + " where " + tenantFilter
        + " \"DEC\" = ? and a_string_array = ? ORDER BY a_date LIMIT 100";
      stmt = conn.prepareStatement(sql);
      if (!multitenant) {
        stmt.setString(counter++, "ORGID");
      }
      stmt.setDouble(counter++, 1.23);
      array = conn.createArrayOf("VARCHAR", strArray);
      stmt.setArray(counter++, array);
      assertPlanDetails(stmt, expectedColNames, expectedColumnNameDataTypes, true, 100);

      if (useIndex) {

        expectedColNames = multitenant
          ? ("\"CF\".\"A_INTEGER\"" + ",\"DEC\"" + ",\"A_STRING_ARRAY\"")
          : ("\"CF\".\"A_INTEGER\"" + ",\"ORGANIZATION_ID\"" + ",\"DEC\"" + ",\"A_STRING_ARRAY\"");
        expectedColumnNameDataTypes = multitenant
          ? ("\"CF\".\"A_INTEGER\"" + " " + "INTEGER" + ",\"DEC\"" + " " + "DECIMAL(10,2)"
            + ",\"A_STRING_ARRAY\"" + " " + "VARCHAR(100) ARRAY")
          : ("\"CF\".\"A_INTEGER\"" + " " + "INTEGER" + ",\"ORGANIZATION_ID\"" + " " + "CHAR(15)"
            + ",\"DEC\"" + " " + "DECIMAL(10,2)" + ",\"A_STRING_ARRAY\"" + " "
            + "VARCHAR(100) ARRAY");

        // Filter on columns that the secondary index is on. No order by. No limit.
        sql = "SELECT a_date FROM " + tableName + " where CF.a_integer = ?";
        stmt = conn.prepareStatement(sql);
        stmt.setInt(1, 1000);
        assertPlanDetails(stmt, expectedColNames, expectedColumnNameDataTypes, false, 0);

        // Filter on columns that the secondary index is on. Order by on the indexed column. Limit
        // specified.
        sql = "SELECT a_date FROM " + tableName
          + " where CF.a_integer = ? ORDER BY CF.a_integer LIMIT 100";
        stmt = conn.prepareStatement(sql);
        stmt.setInt(1, 1000);
        assertPlanDetails(stmt, expectedColNames, expectedColumnNameDataTypes, false, 100);

        // Filter on columns that the secondary index is on. Order by on the non-indexed column.
        // Limit specified.
        sql = "SELECT a_integer FROM " + tableName
          + " where CF.a_integer = ? and a_date = ? ORDER BY a_date LIMIT 100";
        stmt = conn.prepareStatement(sql);
        stmt.setInt(1, 1000);
        stmt.setDate(2, new Date(909000));
        assertPlanDetails(stmt, expectedColNames, expectedColumnNameDataTypes, true, 100);
      }
    } finally {
      conn.close();
    }
  }

  @Test
  public void testAssertQueryAgainstTenantSpecificViewGoesThroughIndex() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl(), new Properties());

    // create table
    conn.createStatement()
      .execute("create table " + "XYZ.ABC" + "   (organization_id char(15) not null, \n"
        + "    entity_id char(15) not null,\n"
        + "    a_string_array varchar(100) array[] not null,\n" + "    b_string varchar(100),\n"
        + "    a_string varchar,\n" + "    a_date date,\n"
        + "    CONSTRAINT pk PRIMARY KEY (organization_id, entity_id, a_string_array)\n" + ")"
        + "MULTI_TENANT=true");

    // create index
    conn.createStatement().execute("CREATE INDEX ABC_IDX ON XYZ.ABC (a_string) INCLUDE (a_date)");

    conn.close();

    // switch to a tenant specific connection
    conn = DriverManager.getConnection(getUrl("tenantId"));

    // create a tenant specific view
    conn.createStatement().execute("CREATE VIEW ABC_VIEW AS SELECT * FROM XYZ.ABC");

    // query against the tenant specific view
    String sql = "SELECT a_date FROM ABC_VIEW where a_string = ?";
    PreparedStatement stmt = conn.prepareStatement(sql);
    stmt.setString(1, "1000");
    QueryPlan plan = stmt.unwrap(PhoenixPreparedStatement.class).optimizeQuery();
    assertEquals("Query should use index", PTableType.INDEX,
      plan.getTableRef().getTable().getType());
  }

  @Test
  @Ignore("PHOENIX-4555 should mark these views as ViewType.READONLY")
  public void testAssertQueryAgainstTenantSpecificViewDoesNotGoThroughIndex() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl(), new Properties());

    // create table
    conn.createStatement()
      .execute("create table " + "XYZ.ABC" + "   (organization_id char(15) not null, \n"
        + "    entity_id char(15) not null,\n"
        + "    a_string_array varchar(100) array[] not null,\n" + "    b_string varchar(100),\n"
        + "    a_string varchar,\n" + "    a_date date,\n"
        + "    CONSTRAINT pk PRIMARY KEY (organization_id, entity_id, a_string_array)\n" + ")"
        + "MULTI_TENANT=true");

    // create index
    conn.createStatement().execute("CREATE INDEX ABC_IDX ON XYZ.ABC (a_string) INCLUDE (a_date)");

    conn.close();

    // switch to a tenant specific connection
    conn = DriverManager.getConnection(getUrl("tenantId"));

    // create a tenant specific view
    conn.createStatement()
      .execute("CREATE VIEW ABC_VIEW AS SELECT * FROM XYZ.ABC where b_string='foo'");

    // query against the tenant specific view
    String sql = "SELECT a_date FROM ABC_VIEW where a_string = ?";
    PreparedStatement stmt = conn.prepareStatement(sql);
    stmt.setString(1, "1000");
    QueryPlan plan = stmt.unwrap(PhoenixPreparedStatement.class).optimizeQuery();
    // should not use index as index does not contain b_string
    assertEquals("Query should not use index", PTableType.VIEW,
      plan.getTableRef().getTable().getType());
  }

  @Test
  public void testDistinctPrefixOnVarcharIndex() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT COUNT(DISTINCT v1) FROM t");
    assertTrue(plan.getGroupBy().isOrderPreserving());
    assertFalse(plan.getGroupBy().getKeyExpressions().isEmpty());
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testDistinctPrefixOnIntIndex() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement()
      .execute("CREATE TABLE t (k INTEGER NOT NULL PRIMARY KEY, v1 INTEGER, v2 VARCHAR)");
    conn.createStatement().execute("CREATE INDEX idx ON t(v1)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery("SELECT COUNT(DISTINCT v1) FROM t");
    assertTrue(plan.getGroupBy().isOrderPreserving());
    assertFalse(plan.getGroupBy().getKeyExpressions().isEmpty());
    assertEquals("IDX", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testTableUsedWithQueryMore() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    conn.createStatement().execute(
      "CREATE TABLE t (k1 CHAR(3) NOT NULL, k2 CHAR(15) NOT NULL, k3 DATE NOT NULL, k4 CHAR(15) NOT NULL, CONSTRAINT pk PRIMARY KEY (k1,k2,k3,k4))");
    conn.createStatement().execute("CREATE INDEX idx ON t(k1,k3,k2,k4)");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery(
      "SELECT * FROM t WHERE (k1,k2,k3,k4) > ('001','001xx000003DHml',to_date('2015-10-21 09:50:55.0'),'017xx0000022FuI')");
    assertEquals("T", plan.getTableRef().getTable().getTableName().getString());
  }

  @Test
  public void testViewUsedWithQueryMoreSalted() throws Exception {
    testViewUsedWithQueryMore(3);
  }

  @Test
  public void testViewUsedWithQueryMoreUnsalted() throws Exception {
    testViewUsedWithQueryMore(null);
  }

  private void testViewUsedWithQueryMore(Integer saltBuckets) throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    int offset = saltBuckets == null ? 0 : 1;
    conn.createStatement().execute("CREATE TABLE MY_TABLES.MY_TABLE "
      + "(ORGANIZATION_ID CHAR(15) NOT NULL, " + "PKCOL1 CHAR(15) NOT NULL,"
      + "PKCOL2 CHAR(15) NOT NULL," + "PKCOL3 CHAR(15) NOT NULL," + "PKCOL4 CHAR(15) NOT NULL,COL1 "
      + "CHAR(15)," + "COL2 CHAR(15)"
      + "CONSTRAINT PK PRIMARY KEY (ORGANIZATION_ID,PKCOL1,PKCOL2,PKCOL3,PKCOL4)) MULTI_TENANT=true"
      + (saltBuckets == null ? "" : (",SALT_BUCKETS=" + saltBuckets)));
    conn.createStatement().execute("CREATE INDEX MY_TABLE_INDEX \n"
      + "ON MY_TABLES.MY_TABLE (PKCOL1, PKCOL3, PKCOL2, PKCOL4)\n" + "INCLUDE (COL1, COL2)");
    Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
    props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, "000000000000000");
    Connection tsconn = DriverManager.getConnection(getUrl(), props);
    tsconn.createStatement()
      .execute("CREATE VIEW MY_TABLE_MT_VIEW AS SELECT * FROM MY_TABLES.MY_TABLE");
    PhoenixStatement stmt = tsconn.createStatement().unwrap(PhoenixStatement.class);
    QueryPlan plan = stmt.optimizeQuery(
      "select * from my_table_mt_view where (pkcol1, pkcol2, pkcol3, pkcol4) > ('0', '0', '0', '0')");
    assertEquals("MY_TABLE_MT_VIEW", plan.getTableRef().getTable().getTableName().getString());

    plan = stmt.compileQuery(
      "select * from my_table_mt_view where (pkcol1, pkcol2) > ('0', '0') and pkcol3 = '000000000000000' and pkcol4 = '000000000000000'");
    assertEquals(3 + offset, plan.getContext().getScanRanges().getBoundPkColumnCount());
    plan = stmt.compileQuery(
      "select * from my_table_mt_view where (pkcol3, pkcol4) > ('0', '0') and pkcol1 = '000000000000000'");
    assertEquals(2 + offset, plan.getContext().getScanRanges().getBoundPkColumnCount());
    plan = stmt.compileQuery(
      "select * from my_table_mt_view where (pkcol1, pkcol2, pkcol3) < ('0', '0', '0')");
    assertEquals(4 + offset, plan.getContext().getScanRanges().getBoundPkColumnCount());
    plan = stmt.compileQuery(
      "select * from my_table_mt_view where (pkcol1, pkcol2, pkcol3) < ('9', '9', '9') and (pkcol1, pkcol2) > ('0', '0')");
    assertEquals(4 + offset, plan.getContext().getScanRanges().getBoundPkColumnCount());
    plan = stmt.compileQuery(
      "select * from my_table_mt_view where pkcol1 = 'a' and pkcol2 = 'b' and pkcol3 = 'c' and (pkcol1, pkcol2) < ('z', 'z')");
    assertEquals(4 + offset, plan.getContext().getScanRanges().getBoundPkColumnCount());
    plan = stmt.compileQuery(
      "select * from my_table_mt_view where (pkcol2, pkcol3) > ('0', '0') and pkcol1 = '000000000000000'");
    assertEquals(4 + offset, plan.getContext().getScanRanges().getBoundPkColumnCount());
  }

  private void assertPlanDetails(PreparedStatement stmt, String expectedPkCols,
    String expectedPkColsDataTypes, boolean expectedHasOrderBy, int expectedLimit)
    throws SQLException {
    Connection conn = stmt.getConnection();
    QueryPlan plan = PhoenixRuntime.getOptimizedQueryPlan(stmt);

    List<Pair<String, String>> columns = PhoenixRuntime.getPkColsForSql(conn, plan);
    assertEquals(expectedPkCols, Joiner.on(",").join(getColumnNames(columns)));
    List<String> dataTypes = new ArrayList<String>();
    columns = new ArrayList<Pair<String, String>>();
    PhoenixRuntime.getPkColsDataTypesForSql(columns, dataTypes, plan, conn, true);

    assertEquals(expectedPkColsDataTypes, appendColNamesDataTypes(columns, dataTypes));
    assertEquals(expectedHasOrderBy, PhoenixRuntime.hasOrderBy(plan));
    assertEquals(expectedLimit, PhoenixRuntime.getLimit(plan));
  }

  private static List<String> getColumnNames(List<Pair<String, String>> columns) {
    List<String> columnNames = new ArrayList<String>(columns.size());
    for (Pair<String, String> col : columns) {
      String familyName = col.getFirst();
      String columnName = col.getSecond();
      if (familyName != null) {
        columnName = familyName + QueryConstants.NAME_SEPARATOR + columnName;
      }
      columnNames.add(columnName);
    }
    return columnNames;
  }

  private String addQuotes(String familyName, String columnNames) {
    Iterable<String> columnNamesList = Splitter.on(",").split(columnNames);
    List<String> quotedColumnNames = new ArrayList<String>();
    for (String columnName : columnNamesList) {
      String quotedColumnName = SchemaUtil.getQuotedFullColumnName(familyName, columnName);
      quotedColumnNames.add(quotedColumnName);
    }
    return Joiner.on(",").join(quotedColumnNames);
  }

  private String appendColNamesDataTypes(List<Pair<String, String>> columns,
    List<String> dataTypes) {
    int size = columns.size();
    assertEquals(size, dataTypes.size()); // they will be equal, but what the heck?
    List<String> pkColsDataTypes = new ArrayList<String>(size);
    for (int i = 0; i < size; i++) {
      String familyName = columns.get(i).getFirst();
      String columnName = columns.get(i).getSecond();
      if (familyName != null) {
        columnName = familyName + QueryConstants.NAME_SEPARATOR + columnName;
      }
      pkColsDataTypes.add(columnName + " " + dataTypes.get(i));
    }
    return Joiner.on(",").join(pkColsDataTypes);
  }

  @Test
  public void testMinMaxQualifierRangeWithOrderByOnKVColumn() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = "testMintestMinMaxQualifierRange".toUpperCase();
    conn.createStatement().execute("CREATE TABLE " + tableName
      + " (k INTEGER NOT NULL PRIMARY KEY, v1 INTEGER, v2 VARCHAR) COLUMN_ENCODED_BYTES=4");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    ResultSet rs = stmt.executeQuery("SELECT K from " + tableName + " ORDER BY (v1)");
    assertQualifierRanges(rs, ENCODED_EMPTY_COLUMN_NAME, ENCODED_CQ_COUNTER_INITIAL_VALUE);
    rs = stmt.executeQuery("SELECT K from " + tableName + " ORDER BY (v1, v2)");
    assertQualifierRanges(rs, ENCODED_EMPTY_COLUMN_NAME, ENCODED_CQ_COUNTER_INITIAL_VALUE + 1);
    rs = stmt.executeQuery("SELECT V2 from " + tableName + " ORDER BY (v1)");
    assertQualifierRanges(rs, ENCODED_EMPTY_COLUMN_NAME, ENCODED_CQ_COUNTER_INITIAL_VALUE + 1);
    rs = stmt.executeQuery("SELECT V1 from " + tableName + " ORDER BY (v1, v2)");
    assertQualifierRanges(rs, ENCODED_EMPTY_COLUMN_NAME, ENCODED_CQ_COUNTER_INITIAL_VALUE + 1);
  }

  @Test
  public void testMinMaxQualifierRangeWithNoOrderBy() throws Exception {
    Connection conn = DriverManager.getConnection(getUrl());
    String tableName = "testMintestMinMaxQualifierRange".toUpperCase();
    conn.createStatement().execute("CREATE TABLE " + tableName
      + " (k INTEGER NOT NULL PRIMARY KEY, v1 INTEGER, v2 VARCHAR) COLUMN_ENCODED_BYTES=4");
    PhoenixStatement stmt = conn.createStatement().unwrap(PhoenixStatement.class);
    ResultSet rs = stmt.executeQuery("SELECT K from " + tableName);
    assertQualifierRangesNotPresent(rs);
    rs = stmt.executeQuery("SELECT V2 from " + tableName);
    assertQualifierRanges(rs, ENCODED_EMPTY_COLUMN_NAME, ENCODED_CQ_COUNTER_INITIAL_VALUE + 1);
    rs = stmt.executeQuery("SELECT V1 from " + tableName);
    assertQualifierRanges(rs, ENCODED_EMPTY_COLUMN_NAME, ENCODED_CQ_COUNTER_INITIAL_VALUE);
  }

  private static void assertQualifierRanges(ResultSet rs, int minQualifier, int maxQualifier)
    throws SQLException {
    Scan scan =
      rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan().getContext().getScan();
    assertNotNull(scan.getAttribute(MIN_QUALIFIER));
    assertNotNull(scan.getAttribute(MAX_QUALIFIER));
    assertEquals(minQualifier, Bytes.toInt(scan.getAttribute(MIN_QUALIFIER)));
    assertEquals(maxQualifier, Bytes.toInt(scan.getAttribute(MAX_QUALIFIER)));
  }

  private static void assertQualifierRangesNotPresent(ResultSet rs) throws SQLException {
    Scan scan =
      rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan().getContext().getScan();
    assertNull(scan.getAttribute(MIN_QUALIFIER));
    assertNull(scan.getAttribute(MAX_QUALIFIER));
  }
}
