/*
 * 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.query.QueryServicesOptions.DEFAULT_USE_STATS_FOR_PARALLELIZATION;
import static org.apache.phoenix.util.PhoenixRuntime.TENANT_ID_ATTRIB;
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 java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;

import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixResultSet;
import org.apache.phoenix.mapreduce.util.PhoenixConfigurationUtil;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableKey;
import org.apache.phoenix.schema.TableNotFoundException;
import org.apache.phoenix.schema.stats.StatisticsUtil;
import org.apache.phoenix.schema.types.PInteger;
import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.EnvironmentEdge;
import org.apache.phoenix.util.EnvironmentEdgeManager;
import org.apache.phoenix.util.PhoenixRuntime;
import org.junit.BeforeClass;
import org.junit.Test;

import com.google.common.collect.Lists;

/**
 * This class has tests for asserting the bytes and rows information exposed in the explain plan
 * when statistics are enabled.
 */
public class ExplainPlanWithStatsEnabledIT extends ParallelStatsEnabledIT {

    private static String tableA;
    private static String tableB;
    private static String tableWithLargeGPWidth;
    private static String indexOnA;
    private static final long largeGpWidth = 2 * 1000 * 1000L;

    @BeforeClass
    public static synchronized void createTables() throws Exception {
        tableA = generateUniqueName();
        initDataAndStats(tableA, 20L);
        tableB = generateUniqueName();
        initDataAndStats(tableB, 20L);
        tableWithLargeGPWidth = generateUniqueName();
        initDataAndStats(tableWithLargeGPWidth, largeGpWidth);
        indexOnA = generateUniqueName();
        createIndex(indexOnA, tableA, 20);
    }

    private static void createIndex(String indexName, String table, long guidePostWidth)
            throws Exception {
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.createStatement().execute(
                "CREATE INDEX " + indexName + " ON " + table + " (c1.a) INCLUDE (c2.b) ");
            conn.createStatement().execute("UPDATE STATISTICS " + indexName);
        }
    }

    private static void initDataAndStats(String tableName, Long guidePostWidth) throws Exception {
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.createStatement()
                    .execute("CREATE TABLE " + tableName
                            + " (k INTEGER PRIMARY KEY, c1.a bigint, c2.b bigint)"
                            + " GUIDE_POSTS_WIDTH=" + guidePostWidth);
            conn.createStatement().execute("upsert into " + tableName + " values (100,1,3)");
            conn.createStatement().execute("upsert into " + tableName + " values (101,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (102,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (103,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (104,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (105,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (106,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (107,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (108,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (109,2,4)");
            conn.commit();
            conn.createStatement().execute("UPDATE STATISTICS " + tableName);
        }
    }

    private static Connection getTenantConnection(String tenantId) throws SQLException {
        String url = getUrl() + ';' + TENANT_ID_ATTRIB + '=' + tenantId;
        return DriverManager.getConnection(url);
    }

    @Test
    public void testBytesRowsForSelectWhenKeyOutOfRange() throws Exception {
        String sql = "SELECT * FROM " + tableA + " where k >= ?";
        List<Object> binds = Lists.newArrayList();
        binds.add(200);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.estimatedBytes);
            assertEquals((Long) 0L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForPointSelectWithLimitGreaterThanPointLookupSize() throws Exception {
        String sql = "SELECT * FROM " + tableA + " where k in (? ,?) limit 4";
        List<Object> binds = Lists.newArrayList();
        binds.add(103); binds.add(104);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 200L, info.estimatedBytes);
            assertEquals((Long) 2L, info.estimatedRows);
            assertEquals((Long) StatisticsUtil.NOT_STATS_BASED_TS, info.estimateInfoTs);
        }
    }

    @Test
    public void testBytesRowsForSelectWithLimit() throws Exception {
        String sql = "SELECT * FROM " + tableA + " where c1.a in (?,?) limit 3";
        String noIndexSQL = "SELECT /*+ NO_INDEX */ * FROM " + tableA + " where c1.a in (?,?) limit 3";
        List<Object> binds = Lists.newArrayList();
        binds.add(1); binds.add(2);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 264L, info.estimatedBytes);
            assertEquals((Long) 3L, info.estimatedRows);
            assertEquals((Long) StatisticsUtil.NOT_STATS_BASED_TS, info.estimateInfoTs);

            info = getByteRowEstimates(conn, noIndexSQL, binds);
            assertEquals((Long) 634L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForSelectWithLimitIgnored() throws Exception {
        String sql = "SELECT * FROM " + tableA + " where (c1.a > c2.b) limit 1";
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            ResultSet rs = conn.createStatement().executeQuery(sql);
            assertFalse(rs.next());
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 390L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);

        }
    }

    @Test
    public void testBytesRowsForSelectWhenKeyInRange() throws Exception {
        String sql = "SELECT * FROM " + tableB + " where k >= ?";
        List<Object> binds = Lists.newArrayList();
        binds.add(99);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 634L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForSelectOnIndex() throws Exception {
        String sql = "SELECT * FROM " + tableA + " where c1.a >= ?";
        List<Object> binds = Lists.newArrayList();
        binds.add(0);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            try (PreparedStatement statement = conn.prepareStatement(sql)) {
                int paramIdx = 1;
                for (Object bind : binds) {
                    statement.setObject(paramIdx++, bind);
                }
                ResultSet rs = statement.executeQuery(sql);
                assertTrue(rs.next());
                assertEquals(100, rs.getInt(1));
                assertEquals(1, rs.getInt(2));
                assertEquals(3, rs.getInt(3));
                assertTrue(rs.next());
            }
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 390L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForUnion() throws Exception {
        String sql =
                "SELECT /*+ NO_INDEX */ * FROM " + tableA + " UNION ALL SELECT * FROM " + tableB;
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, Lists.newArrayList());
            assertEquals((Long) (2 * 634L), info.estimatedBytes);
            assertEquals((Long) (2 * 10L), info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testEstimatesForUnionWithTableWithLargeGpWidth() throws Exception {
        // For table with largeGpWidth, a guide post is generated that has the
        // byte size estimate of guide post width.
        String sql =
                "SELECT /*+ NO_INDEX */ * FROM " + tableA + " UNION ALL SELECT * FROM " + tableB
                        + " UNION ALL SELECT * FROM " + tableWithLargeGPWidth;
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, Lists.newArrayList());
            assertEquals((Long) (2 * 634 + largeGpWidth), info.estimatedBytes);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForHashJoin() throws Exception {
        String sql =
                "SELECT ta.c1.a, ta.c2.b FROM " + tableA + " ta JOIN " + tableB
                        + " tb ON ta.k = tb.k";
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, Lists.newArrayList());
            assertEquals((Long) (634L), info.estimatedBytes);
            assertEquals((Long) (10L), info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForSortMergeJoin() throws Exception {
        String sql =
                "SELECT /*+ NO_INDEX USE_SORT_MERGE_JOIN */ ta.c1.a, ta.c2.b FROM " + tableA
                        + " ta JOIN " + tableB + " tb ON ta.k = tb.k";
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, Lists.newArrayList());
            assertEquals((Long) (2 * 634L), info.estimatedBytes);
            assertEquals((Long) (2 * 10L), info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForAggregateQuery() throws Exception {
        String sql = "SELECT count(*) FROM " + tableA + " where k >= ?";
        List<Object> binds = Lists.newArrayList();
        binds.add(99);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 634L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForUpsertSelectServerSide() throws Exception {
        String sql = "UPSERT INTO " + tableA + " SELECT * FROM " + tableB;
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.setAutoCommit(true);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 634L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForUpsertSelectClientSide() throws Exception {
        String sql = "UPSERT INTO " + tableB + " SELECT * FROM " + tableB;
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.setAutoCommit(false);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 634L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForUpsertValues() throws Exception {
        String sql = "UPSERT INTO " + tableA + " VALUES (?, ?, ?)";
        List<Object> binds = Lists.newArrayList();
        binds.add(99);
        binds.add(99);
        binds.add(99);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.estimatedBytes);
            assertEquals((Long) 0L, info.estimatedRows);
            assertEquals((Long) 0L, info.estimateInfoTs);
        }
    }

    @Test
    public void testBytesRowsForDeleteServerSide() throws Exception {
        String sql = "DELETE FROM " + tableA + " where k >= ?";
        List<Object> binds = Lists.newArrayList();
        binds.add(99);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.setAutoCommit(true);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 634L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertTrue(info.estimateInfoTs > 0);
        }
    }

    @Test
    public void testBytesRowsForDeleteClientSideExecutedSerially() throws Exception {
        String sql = "DELETE FROM " + tableA + " where k >= ? LIMIT 2";
        List<Object> binds = Lists.newArrayList();
        binds.add(99);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.setAutoCommit(false);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 200L, info.estimatedBytes);
            assertEquals((Long) 2L, info.estimatedRows);
            assertEquals((Long) StatisticsUtil.NOT_STATS_BASED_TS, info.estimateInfoTs);
        }
    }

    @Test
    public void testBytesRowsForPointDelete() throws Exception {
        String sql = "DELETE FROM " + tableA + " where k = ?";
        List<Object> binds = Lists.newArrayList();
        binds.add(100);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.setAutoCommit(false);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.estimatedBytes);
            assertEquals((Long) 0L, info.estimatedRows);
            assertEquals((Long) 0L, info.estimateInfoTs);
        }
    }

    @Test
    public void testBytesRowsForSelectExecutedSerially() throws Exception {
        String sql = "SELECT * FROM " + tableA + " LIMIT 2";
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.setAutoCommit(false);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 176L, info.estimatedBytes);
            assertEquals((Long) 2L, info.estimatedRows);
            assertEquals((Long) StatisticsUtil.NOT_STATS_BASED_TS, info.estimateInfoTs);
        }
    }

    public static class Estimate {
        final Long estimatedBytes;
        final Long estimatedRows;
        final Long estimateInfoTs;

        public Long getEstimatedBytes() {
            return estimatedBytes;
        }

        public Long getEstimatedRows() {
            return estimatedRows;
        }

        public Long getEstimateInfoTs() {
            return estimateInfoTs;
        }

        Estimate(Long rows, Long bytes, Long ts) {
            this.estimatedBytes = bytes;
            this.estimatedRows = rows;
            this.estimateInfoTs = ts;
        }
    }

    public static Estimate getByteRowEstimates(Connection conn, String sql, List<Object> bindValues)
            throws Exception {
        String explainSql = "EXPLAIN " + sql;
        Long estimatedBytes = null;
        Long estimatedRows = null;
        Long estimateInfoTs = null;
        try (PreparedStatement statement = conn.prepareStatement(explainSql)) {
            int paramIdx = 1;
            for (Object bind : bindValues) {
                statement.setObject(paramIdx++, bind);
            }
            ResultSet rs = statement.executeQuery(explainSql);
            rs.next();
            estimatedBytes =
                    (Long) rs.getObject(PhoenixRuntime.EXPLAIN_PLAN_ESTIMATED_BYTES_READ_COLUMN);
            estimatedRows =
                    (Long) rs.getObject(PhoenixRuntime.EXPLAIN_PLAN_ESTIMATED_ROWS_READ_COLUMN);
            estimateInfoTs =
                    (Long) rs.getObject(PhoenixRuntime.EXPLAIN_PLAN_ESTIMATE_INFO_TS_COLUMN);
        }
        return new Estimate(estimatedRows, estimatedBytes, estimateInfoTs);
    }

    @Test
    public void testSettingUseStatsForParallelizationProperty() throws Exception {
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            String table = generateUniqueName();
            String ddl =
                    "CREATE TABLE " + table
                            + " (PK1 INTEGER NOT NULL PRIMARY KEY, KV1 VARCHAR) USE_STATS_FOR_PARALLELIZATION = false";
            conn.createStatement().execute(ddl);
            assertUseStatsForQueryFlag(table, conn.unwrap(PhoenixConnection.class), false);

            ddl = "ALTER TABLE " + table + " SET USE_STATS_FOR_PARALLELIZATION = true";
            conn.createStatement().execute(ddl);
            assertUseStatsForQueryFlag(table, conn.unwrap(PhoenixConnection.class), true);

            table = generateUniqueName();
            ddl =
                    "CREATE TABLE " + table
                            + " (PK1 INTEGER NOT NULL PRIMARY KEY, KV1 VARCHAR) USE_STATS_FOR_PARALLELIZATION = false";
            conn.createStatement().execute(ddl);
            assertUseStatsForQueryFlag(table, conn.unwrap(PhoenixConnection.class), false);

            table = generateUniqueName();
            ddl = "CREATE TABLE " + table + " (PK1 INTEGER NOT NULL PRIMARY KEY, KV1 VARCHAR)";
            conn.createStatement().execute(ddl);

            // because we didn't set the property, PTable.useStatsForParallelization() should return
            // null
            assertUseStatsForQueryFlag(table, conn.unwrap(PhoenixConnection.class), null);
        }
    }

    private static void assertUseStatsForQueryFlag(String tableName, PhoenixConnection conn,
            Boolean expected) throws TableNotFoundException, SQLException {
        assertEquals(expected,
            conn.unwrap(PhoenixConnection.class).getMetaDataCache()
                    .getTableRef(new PTableKey(null, tableName)).getTable()
                    .useStatsForParallelization());
        String query =
                "SELECT USE_STATS_FOR_PARALLELIZATION FROM SYSTEM.CATALOG WHERE TABLE_NAME = ? AND COLUMN_NAME IS NULL AND COLUMN_FAMILY IS NULL AND TENANT_ID IS NULL";
        PreparedStatement stmt = conn.prepareStatement(query);
        stmt.setString(1, tableName);
        ResultSet rs = stmt.executeQuery();
        rs.next();
        boolean b = rs.getBoolean(1);
        if (expected == null) {
            assertTrue(rs.wasNull());
        } else {
            assertEquals(expected, b);
        }
    }

    @Test
    public void testBytesRowsForSelectOnTenantViews() throws Exception {
        String tenant1View = generateUniqueName();
        String tenant2View = generateUniqueName();
        String tenant3View = generateUniqueName();
        String tenant4View = generateUniqueName();
        String multiTenantBaseTable = generateUniqueName();
        String tenant1 = "tenant1";
        String tenant2 = "tenant2";
        String tenant3 = "tenant3";
        String tenant4 = "tenant4";
        MyClock clock = new MyClock(1000);
        
        createMultitenantTableAndViews(tenant1View, tenant2View, tenant3View, tenant4View, tenant1, tenant2,
            tenant3, tenant4, multiTenantBaseTable, clock);
        // query the entire multitenant table
        String sql = "SELECT * FROM " + multiTenantBaseTable + " WHERE ORGID >= ?";
        List<Object> binds = Lists.newArrayList();
        binds.add("tenant0");
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 681L, info.estimatedBytes);
            assertEquals((Long) 10L, info.estimatedRows);
            assertNull(info.estimateInfoTs); // unknown/null because region (* - tenant1) has no guideposts
        }
        binds.clear();
        long prevTenantBytes;
        // query tenant1 view
        try (Connection conn = getTenantConnection(tenant1)) {
            sql = "SELECT * FROM " + tenant1View;
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 119L, info.estimatedBytes);
            assertEquals((Long) 2L, info.estimatedRows);
            assertNull(info.estimateInfoTs); // unknown/null because scan occurs in first region because of start key versus slightly larger region boundary
        }
        // query tenant2 view
        try (Connection conn = getTenantConnection(tenant2)) {
            sql = "SELECT * FROM " + tenant2View;
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) (prevTenantBytes=119L), info.estimatedBytes);
            assertEquals((Long) 2L, info.estimatedRows);
            assertEquals((Long) clock.currentTime(), info.estimateInfoTs);
        }
        // query tenant3 view
        try (Connection conn = getTenantConnection(tenant3)) {
            sql = "SELECT * FROM " + tenant3View;
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 443L, info.estimatedBytes);
            assertEquals((Long) 6L, info.estimatedRows);
            assertEquals((Long) clock.currentTime(), info.estimateInfoTs);
        }
        /*
         * Now we will add some rows to tenant1view an run update stats on it. We will do this after
         * advancing our clock by 1000 seconds. This way we can check that only the region for
         * tenant1 will have updated guidepost with the new timestamp.
         */
        long prevGuidePostTimestamp = clock.currentTime();
        clock.advanceTime(1000);
        try {
            EnvironmentEdgeManager.injectEdge(clock);
            // Update tenant1 view
            try (Connection conn = getTenantConnection(tenant2)) {
                // upsert a few rows for tenantView
                clock.setAdvance(false);
                conn.createStatement()
                        .executeUpdate("UPSERT INTO " + tenant2View + " VALUES (11, 11, 11)");
                conn.createStatement()
                        .executeUpdate("UPSERT INTO " + tenant2View + " VALUES (12, 12, 12)");
                conn.createStatement()
                        .executeUpdate("UPSERT INTO " + tenant2View + " VALUES (13, 13, 13)");
                conn.createStatement()
                        .executeUpdate("UPSERT INTO " + tenant2View + " VALUES (14, 14, 14)");
                conn.createStatement()
                        .executeUpdate("UPSERT INTO " + tenant2View + " VALUES (15, 15, 15)");
                conn.createStatement()
                        .executeUpdate("UPSERT INTO " + tenant2View + " VALUES (16, 16, 16)");
                conn.commit();
                // run update stats on the tenantView
                conn.createStatement().executeUpdate("UPDATE STATISTICS " + tenant2View);
                // get estimates now and check if they were updated as expected
                sql = "SELECT * FROM " + tenant2View;
                Estimate info = getByteRowEstimates(conn, sql, Collections.emptyList());
                assertTrue(info.estimatedBytes > prevTenantBytes);
                assertEquals((Long) 8L, info.estimatedRows);
                assertEquals((Long) clock.currentTime(), info.estimateInfoTs);
            }
        } finally {
            EnvironmentEdgeManager.reset();
        }
        // Now check estimates again for tenantView1 and tenantView3. They should stay the same.
        try (Connection conn = getTenantConnection(tenant1)) {
            sql = "SELECT * FROM " + tenant1View;
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 119L, info.estimatedBytes);
            assertEquals((Long) 2L, info.estimatedRows);
            assertNull(info.estimateInfoTs);
        }
        try (Connection conn = getTenantConnection(tenant3)) {
            sql = "SELECT * FROM " + tenant3View;
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 443L, info.estimatedBytes);
            assertEquals((Long) 6L, info.estimatedRows);
            assertEquals((Long) prevGuidePostTimestamp, info.estimateInfoTs);
        }
        /*
         * Now let's query the base table and see estimates. Because we use the minimum timestamp
         * for all guideposts that we will be scanning, the timestamp for the estimate info for this
         * query should be prevGuidePostTimestamp.
         */
        binds.clear();
        binds.add("tenant0");
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            sql = "SELECT * FROM " + multiTenantBaseTable + " WHERE ORGID >= ?";
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 1167L, info.estimatedBytes);
            assertEquals((Long) 16L, info.estimatedRows);
            assertNull(info.estimateInfoTs);
        }
        // query tenant4 view
        binds.clear();
        try (Connection conn = getTenantConnection(tenant4)) {
            sql = "SELECT * FROM " + tenant4View;
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) (prevTenantBytes=0L), info.estimatedBytes);
            assertEquals((Long) 0L, info.estimatedRows);
            assertNull(info.estimateInfoTs); // Unknown b/c second region of tenant4 has no gps
        }
        clock.advanceTime(1000);
        try {
            EnvironmentEdgeManager.injectEdge(clock);
            // Update tenant4 view
            try (Connection conn = getTenantConnection(tenant4)) {
                // upsert a few rows for tenantView
                conn.createStatement()
                    .executeUpdate("UPSERT INTO " + tenant4View + " VALUES (6, 17,17)");
                conn.createStatement()
                    .executeUpdate("UPSERT INTO " + tenant4View + " VALUES (7, 17,17)");
                conn.commit();
                // run update stats on the tenantView
                conn.createStatement().executeUpdate("UPDATE STATISTICS " + tenant4View);
                // get estimates now and check if they were updated as expected
                sql = "SELECT * FROM " + tenant4View;
                Estimate info = getByteRowEstimates(conn, sql, Collections.emptyList());
                assertTrue(info.estimatedBytes > prevTenantBytes);
                assertEquals((Long) 119L, info.estimatedBytes);
                assertEquals((Long) 2L, info.estimatedRows);
                assertEquals((Long) clock.currentTime(), info.estimateInfoTs);
                sql = "SELECT * FROM " + tenant4View + " WHERE pk2 >= 6";
                info = getByteRowEstimates(conn, sql, Collections.emptyList());
                assertEquals((Long) 119L, info.estimatedBytes);
                assertEquals((Long) 2L, info.estimatedRows);
                assertEquals((Long) clock.currentTime(), info.estimateInfoTs);
            }
        } finally {
            EnvironmentEdgeManager.reset();
        }
    }

    @Test // See https://issues.apache.org/jira/browse/PHOENIX-4287
    public void testEstimatesForAggregateQueries() throws Exception {
        String tableName = generateUniqueName();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            int guidePostWidth = 20;
            String ddl =
                    "CREATE TABLE " + tableName + " (k INTEGER PRIMARY KEY, a bigint, b bigint)"
                            + " GUIDE_POSTS_WIDTH=" + guidePostWidth + " SPLIT ON (102, 105, 108)";
            conn.createStatement().execute(ddl);
            conn.createStatement().execute("upsert into " + tableName + " values (100,1,3)");
            conn.createStatement().execute("upsert into " + tableName + " values (101,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (102,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (103,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (104,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (105,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (106,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (107,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (108,2,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (109,2,4)");
            conn.commit();
            conn.createStatement().execute("UPDATE STATISTICS " + tableName + "");
        }
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            String sql = "SELECT COUNT(*) " + " FROM " + tableName;
            // We don't have the use stats for parallelization property
            // set on the table. In this case, we end up defaulting to the
            // value set in config which is true.
            ResultSet rs = conn.createStatement().executeQuery(sql);
            // stats are being used for parallelization. So number of scans is higher.
            assertEquals(11, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getScans().get(0).size());
            assertTrue(rs.next());
            assertEquals(10, rs.getInt(1));
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Now, let's disable USE_STATS_FOR_PARALLELIZATION on the table
            conn.createStatement().execute(
                "ALTER TABLE " + tableName + " SET USE_STATS_FOR_PARALLELIZATION = " + false);
            rs = conn.createStatement().executeQuery(sql);
            // stats are not being used for parallelization. So number of scans is lower.
            assertEquals(4, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getScans().get(0).size());
            assertTrue(rs.next());
            assertEquals(10, rs.getInt(1));
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertTrue(info.getEstimateInfoTs() > 0);

            // assert that the aggregate query on view also works correctly
            String viewName = "V_" + generateUniqueName();
            conn.createStatement().execute("CREATE VIEW " + viewName + " AS SELECT * FROM "
                    + tableName + " USE_STATS_FOR_PARALLELIZATION = false");
            sql = "SELECT COUNT(*) FROM " + viewName;
            rs = conn.createStatement().executeQuery(sql);
            // stats are not being used for parallelization. So number of scans is lower.
            assertEquals(4, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getScans().get(0).size());
            assertTrue(rs.next());
            assertEquals(10, rs.getInt(1));
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Now let's make sure that when using stats for parallelization, our estimates
            // and query results stay the same for view and base table
            conn.createStatement().execute(
                "ALTER TABLE " + tableName + " SET USE_STATS_FOR_PARALLELIZATION=true");
            sql = "SELECT COUNT(*) FROM " + tableName;
            // query the table
            rs = conn.createStatement().executeQuery(sql);
            // stats are being used for parallelization. So number of scans is higher.
            assertEquals(11, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getScans().get(0).size());
            assertTrue(rs.next());
            assertEquals(10, rs.getInt(1));
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertTrue(info.getEstimateInfoTs() > 0);

            conn.createStatement()
                    .execute("ALTER VIEW " + viewName + " SET USE_STATS_FOR_PARALLELIZATION=true");
            sql = "SELECT COUNT(*) FROM " + viewName;
            // query the view
            rs = conn.createStatement().executeQuery(sql);
            // stats are not being used for parallelization. So number of scans is higher.
            assertEquals(11, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getScans().get(0).size());
            assertTrue(rs.next());
            assertEquals(10, rs.getInt(1));
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertTrue(info.getEstimateInfoTs() > 0);
        }
    }

    @Test
    public void testSelectQueriesWithStatsForParallelizationOff() throws Exception {
        testSelectQueriesWithFilters(false);
    }

    @Test
    public void testSelectQueriesWithStatsForParallelizationOn() throws Exception {
        testSelectQueriesWithFilters(true);
    }

    private void testSelectQueriesWithFilters(boolean useStatsForParallelization) throws Exception {
        String tableName = generateUniqueName();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            int guidePostWidth = 20;
            String ddl =
                    "CREATE TABLE " + tableName + " (k INTEGER PRIMARY KEY, a bigint, b bigint)"
                            + " GUIDE_POSTS_WIDTH=" + guidePostWidth
                            + ", USE_STATS_FOR_PARALLELIZATION=" + useStatsForParallelization + " SPLIT ON (102, 105, 108)";
            conn.createStatement().execute(ddl);
            conn.createStatement().execute("upsert into " + tableName + " values (100,100,3)");
            conn.createStatement().execute("upsert into " + tableName + " values (101,101,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (102,102,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (103,103,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (104,104,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (105,105,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (106,106,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (107,107,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (108,108,4)");
            conn.createStatement().execute("upsert into " + tableName + " values (109,109,4)");
            conn.commit();
            conn.createStatement().execute("UPDATE STATISTICS " + tableName + "");
        }
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            // query whose start key is before any data
            String sql = "SELECT a FROM " + tableName + " WHERE K >= 99";
            ResultSet rs = conn.createStatement().executeQuery(sql);
            int i = 0;
            int numRows = 10;
            while (rs.next()) {
                assertEquals(100 + i, rs.getInt(1));
                i++;
            }
            assertEquals(numRows, i);
            Estimate info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertEquals((Long) 720L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);

            // query whose start key is after any data
            sql = "SELECT a FROM " + tableName + " WHERE K >= 110";
            rs = conn.createStatement().executeQuery(sql);
            assertFalse(rs.next());
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.getEstimatedRows());
            assertEquals((Long) 0L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Query whose end key is before any data
            sql = "SELECT a FROM " + tableName + " WHERE K <= 98";
            rs = conn.createStatement().executeQuery(sql);
            assertFalse(rs.next());
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.getEstimatedRows());
            assertEquals((Long) 0L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Query whose end key is after any data. In this case, we return the estimate as
            // scanning all the guide posts.
            sql = "SELECT a FROM " + tableName + " WHERE K <= 110";
            rs = conn.createStatement().executeQuery(sql);
            i = 0;
            numRows = 10;
            while (rs.next()) {
                assertEquals(100 + i, rs.getInt(1));
                i++;
            }
            assertEquals(numRows, i);
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertEquals((Long) 720L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Query whose start key and end key is before any data. In this case,
            // we return the estimate as scanning the first guide post
            sql = "SELECT a FROM " + tableName + " WHERE K <= 90 AND K >= 80";
            rs = conn.createStatement().executeQuery(sql);
            assertFalse(rs.next());
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.getEstimatedRows());
            assertEquals((Long) 0L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Query whose start key and end key is after any data. In this case, we return the
            // estimate as
            // scanning no guide post
            sql = "SELECT a FROM " + tableName + " WHERE K <= 130 AND K >= 120";
            rs = conn.createStatement().executeQuery(sql);
            assertFalse(rs.next());
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 0L, info.getEstimatedRows());
            assertEquals((Long) 0L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);

            // Query whose start key is before and end key is between data. In this case, we return
            // the estimate as scanning no guide post
            sql = "SELECT a FROM " + tableName + " WHERE K <= 102 AND K >= 90";
            rs = conn.createStatement().executeQuery(sql);
            i = 0;
            numRows = 3;
            while (rs.next()) {
                assertEquals(100 + i, rs.getInt(1));
                i++;
            }
            info = getByteRowEstimates(conn, sql, binds);
            // Depending on the guidepost boundary, this estimate
            // can be slightly off. It's called estimate for a reason.
            assertEquals((Long) 3L, info.getEstimatedRows());
            assertEquals((Long) 160L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);
            // Query whose start key is between and end key is after data.
            sql = "SELECT a FROM " + tableName + " WHERE K <= 120 AND K >= 100";
            rs = conn.createStatement().executeQuery(sql);
            i = 0;
            numRows = 10;
            while (rs.next()) {
                assertEquals(100 + i, rs.getInt(1));
                i++;
            }
            info = getByteRowEstimates(conn, sql, binds);
            // Depending on the guidepost boundary, this estimate
            // can be slightly off. It's called estimate for a reason.
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertEquals((Long) 720L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);
            // Query whose start key and end key are both between data.
            sql = "SELECT a FROM " + tableName + " WHERE K <= 109 AND K >= 100";
            rs = conn.createStatement().executeQuery(sql);
            i = 0;
            numRows = 10;
            while (rs.next()) {
                assertEquals(100 + i, rs.getInt(1));
                i++;
            }
            info = getByteRowEstimates(conn, sql, binds);
            // Depending on the guidepost boundary, this estimate
            // can be slightly off. It's called estimate for a reason.
            assertEquals((Long) 10L, info.getEstimatedRows());
            assertEquals((Long) 720L, info.getEstimatedBytes());
            assertTrue(info.getEstimateInfoTs() > 0);
        }
    }

    private static void createMultitenantTableAndViews(String tenant1View, String tenant2View,
            String tenant3View, String tenant4View, String tenant1, String tenant2, String tenant3, String tenant4,
            String multiTenantTable, MyClock clock) throws Exception {
        byte[][] splits =
                new byte[][] {
                    ByteUtil.concat(Bytes.toBytes(tenant1),PInteger.INSTANCE.toBytes(1)),
                    ByteUtil.concat(Bytes.toBytes(tenant2),PInteger.INSTANCE.toBytes(1)),
                    ByteUtil.concat(Bytes.toBytes(tenant3),PInteger.INSTANCE.toBytes(1)),
                    ByteUtil.concat(Bytes.toBytes(tenant4),PInteger.INSTANCE.toBytes(6)),
                    };
        String ddl =
                "CREATE TABLE " + multiTenantTable
                        + " (orgId CHAR(7) NOT NULL, pk2 integer NOT NULL, c1.a bigint, c2.b bigint CONSTRAINT PK PRIMARY KEY "
                        + "(ORGID, PK2)) MULTI_TENANT=true, GUIDE_POSTS_WIDTH=2";
        // Use our own clock to get rows created with our controlled timestamp
        try {
            EnvironmentEdgeManager.injectEdge(clock);
            try (Connection conn = DriverManager.getConnection(getUrl())) {
                PreparedStatement stmt = conn.prepareStatement(ddl + " SPLIT ON (?,?,?,?)");
                for (int i = 0; i < splits.length; i++) {
                    stmt.setBytes(i+1, splits[i]);
                }
                stmt.executeUpdate();
                clock.advanceTime(1000);
                /**
                 * Insert 2 rows each for tenant1 and tenant2 and 6 rows for tenant3
                 */
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant1 + "',1,1,1)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant1 + "',2,2,2)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant2 + "',1,3,3)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant2 + "',2,4,4)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant3 + "',1,5,5)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant3 + "',2,6,6)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant3 + "',3,7,7)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant3 + "',4,8,8)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant3 + "',5,9,9)");
                conn.createStatement().execute(
                    "upsert into " + multiTenantTable + " values ('" + tenant3 + "',6,10,10)");
                conn.commit();
            }
            clock.setAdvance(false);
            try (Connection conn = getTenantConnection(tenant1)) {
                conn.createStatement().execute(
                    "CREATE VIEW " + tenant1View + " AS SELECT * FROM " + multiTenantTable);
                conn.createStatement().execute("UPDATE STATISTICS " + tenant1View);
            }
            try (Connection conn = getTenantConnection(tenant2)) {
                conn.createStatement().execute(
                    "CREATE VIEW " + tenant2View + " AS SELECT * FROM " + multiTenantTable);
                conn.createStatement().execute("UPDATE STATISTICS " + tenant2View);
            }
            try (Connection conn = getTenantConnection(tenant3)) {
                conn.createStatement().execute(
                    "CREATE VIEW " + tenant3View + " AS SELECT * FROM " + multiTenantTable);
                conn.createStatement().execute("UPDATE STATISTICS " + tenant3View);
            }
            try (Connection conn = getTenantConnection(tenant4)) {
                conn.createStatement().execute(
                    "CREATE VIEW " + tenant4View + " AS SELECT * FROM " + multiTenantTable);
            }
        } finally {
            EnvironmentEdgeManager.reset();
        }
    }

    private static class MyClock extends EnvironmentEdge {
        public volatile long time;
        private boolean shouldAdvance = true;

        public MyClock(long time) {
            this.time = time;
        }

        @Override
        public long currentTime() {
            if(shouldAdvance) {
                return time++;
            } else {
                return time;
            }
        }
        public void setAdvance(boolean val) {
            shouldAdvance = val;
        }
        public void advanceTime(long t) {
            this.time += t;
        }
    }

    @Test
    public void testPartialStatsForTenantViews() throws Exception {
        String tenant1View = generateUniqueName();
        String tenant2View = generateUniqueName();
        String multiTenantTable = generateUniqueName();
        String tenantId1 = "00Dabcdetenant1";
        String tenantId2 = "00Dabcdetenant2";

        String ddl =
                "CREATE TABLE " + multiTenantTable
                        + " (orgId CHAR(15) NOT NULL, pk2 CHAR(3) NOT NULL, a bigint, b bigint CONSTRAINT PK PRIMARY KEY "
                        + "(ORGID, PK2)) MULTI_TENANT=true, GUIDE_POSTS_WIDTH=20";
        createTestTable(getUrl(), ddl, null, null);
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            // split such that some data for view2 resides on region of view1
            try (Admin admin =
                    conn.unwrap(PhoenixConnection.class).getQueryServices().getAdmin()) {
                byte[] splitKey = Bytes.toBytes("00Dabcdetenant200B");
                admin.split(TableName.valueOf(multiTenantTable), splitKey);
            }

            /**
             * Insert 2 rows for tenant1 and 6 for tenant2
             */
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId1 + "','00A',1,1)");
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId1 + "','00B',2,2)");
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId2 + "','00A',3,3)");
            // We split at tenant2 + 00B. So the following rows will reside in a different region
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId2 + "','00B',4,4)");
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId2 + "','00C',5,5)");
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId2 + "','00D',6,6)");
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId2 + "','00E',7,7)");
            conn.createStatement().execute(
                "upsert into " + multiTenantTable + " values ('" + tenantId2 + "','00F',8,8)");
            conn.commit();
        }
        try (Connection conn = getTenantConnection(tenantId1)) {
            conn.createStatement().execute(
                "CREATE VIEW " + tenant1View + " AS SELECT * FROM " + multiTenantTable);
        }
        try (Connection conn = getTenantConnection(tenantId2)) {
            conn.createStatement().execute(
                "CREATE VIEW " + tenant2View + " AS SELECT * FROM " + multiTenantTable);
        }
        String sql = "";
        List<Object> binds = Lists.newArrayList();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            /*
             * I have seen compaction running and generating stats for the second region of
             * tenant2View So let's disable compaction on the table, delete any stats we have
             * collected in SYSTEM.STATS table, clear cache and run update stats to make sure our
             * test gets a deterministic setup.
             */
            String disableCompaction =
                    "ALTER TABLE " + multiTenantTable + " SET COMPACTION_ENABLED = false";
            conn.createStatement().executeUpdate(disableCompaction);
            String delete =
                    "DELETE FROM SYSTEM.STATS WHERE PHYSICAL_NAME = '" + multiTenantTable + "'";
            conn.createStatement().executeUpdate(delete);
            conn.commit();
            conn.unwrap(PhoenixConnection.class).getQueryServices().clearCache();
        }
        // Now let's run update stats on tenant1View
        try (Connection conn = getTenantConnection(tenantId1)) {
            conn.createStatement().execute("UPDATE STATISTICS " + tenant1View);
        }
        // query tenant2 view
        try (Connection conn = getTenantConnection(tenantId2)) {
            sql = "SELECT * FROM " + tenant2View;

            Estimate info = getByteRowEstimates(conn, sql, binds);
            /*
             * Because we ran update stats only for tenant1View, there is only partial guidepost
             * info available for tenant2View.
             */
            assertEquals((Long) 1L, info.estimatedRows);
            // ok now run update stats for tenant2 view
            conn.createStatement().execute("UPDATE STATISTICS " + tenant2View);
            /*
             * And now, let's recheck our estimate info. We should have all the rows of view2
             * available now.
             */
            info = getByteRowEstimates(conn, sql, binds);
            assertEquals((Long) 6L, info.estimatedRows);
        }
    }

    @Test
    public void testIndexesUseStatsIfOnForParentTable() throws Exception {
        testIndexesInheritUseStatsPropFromParentTable(true);
    }

    @Test
    public void testIndexesDontUseStatsIfOffForParentTable() throws Exception {
        testIndexesInheritUseStatsPropFromParentTable(false);
    }

    private void testIndexesInheritUseStatsPropFromParentTable(boolean useStats) throws Exception {
        String baseTable = generateUniqueName();
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            String ddl =
                    "CREATE TABLE " + baseTable
                            + " (k INTEGER PRIMARY KEY, a bigint, b bigint, c bigint) GUIDE_POSTS_WIDTH=20, USE_STATS_FOR_PARALLELIZATION="
                            + useStats;
            conn.createStatement().execute(ddl);
            conn.createStatement().execute("upsert into " + baseTable + " values (100,1,1,1)");
            conn.createStatement().execute("upsert into " + baseTable + " values (101,2,2,2)");
            conn.createStatement().execute("upsert into " + baseTable + " values (102,3,3,3)");
            conn.createStatement().execute("upsert into " + baseTable + " values (103,4,4,4)");
            conn.createStatement().execute("upsert into " + baseTable + " values (104,5,5,5)");
            conn.createStatement().execute("upsert into " + baseTable + " values (105,6,6,6)");
            conn.createStatement().execute("upsert into " + baseTable + " values (106,7,7,7)");
            conn.createStatement().execute("upsert into " + baseTable + " values (107,8,8,8)");
            conn.createStatement().execute("upsert into " + baseTable + " values (108,9,9,9)");
            conn.createStatement().execute("upsert into " + baseTable + " values (109,10,10,10)");
            conn.commit();

            // Create global index on base table
            String globalIndex = "GI_" + generateUniqueName();
            ddl = "CREATE INDEX " + globalIndex + " ON " + baseTable + " (a) INCLUDE (b) ";
            conn.createStatement().execute(ddl);

            // Create local index on base table
            String localIndex = "LI_" + generateUniqueName();
            ddl = "CREATE LOCAL INDEX " + localIndex + " ON " + baseTable + " (b) INCLUDE (c) ";
            conn.createStatement().execute(ddl);

            // Create a view and an index on it
            String view = "V_" + generateUniqueName();
            ddl =
                    "CREATE VIEW " + view + " AS SELECT * FROM " + baseTable
                            + " USE_STATS_FOR_PARALLELIZATION=" + useStats;
            conn.createStatement().execute(ddl);
            String viewIndex = "VI_" + generateUniqueName();
            ddl = "CREATE INDEX " + viewIndex + " ON " + view + " (b)";
            conn.createStatement().execute(ddl);

            // collect stats for all
            conn.createStatement().execute("UPDATE STATISTICS " + baseTable);

            // query against the base table
            String query = "SELECT /*+ NO_INDEX */ COUNT(*) FROM " + baseTable;
            PhoenixResultSet rs =
                    conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against base table
            assertEquals(baseTable,
                rs.getStatement().getQueryPlan().getTableRef().getTable().getName().getString());
            assertEquals(useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());

            // query against the global index
            query = "SELECT B FROM " + baseTable + " WHERE A > 0";
            rs = conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against global index
            assertEquals(globalIndex, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getTableRef().getTable().getName().getString());
            assertEquals(useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());

            // query against the local index
            query = "SELECT C FROM " + baseTable + " WHERE B > 0";
            rs = conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against global index
            assertEquals(localIndex, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getTableRef().getTable().getName().getString());
            assertEquals(useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());

            // query against the view
            query = "SELECT * FROM " + view;
            rs = conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against view
            assertEquals(view, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getTableRef().getTable().getName().getString());
            assertEquals(useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());

            // query against the view index
            query = "SELECT 1 FROM " + view + " WHERE B > 0";
            rs = conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against viewIndex
            assertEquals(viewIndex, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getTableRef().getTable().getName().getString());
            assertEquals(useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());

            // flip the use stats property on the view and see if view index picks it up
            conn.createStatement().execute(
                "ALTER VIEW " + view + " SET USE_STATS_FOR_PARALLELIZATION=" + !useStats);

            // query against the view
            query = "SELECT * FROM " + view;
            rs = conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against view
            assertEquals(view, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getTableRef().getTable().getName().getString());
            assertEquals(!useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());

            // query against the view index
            query = "SELECT 1 FROM " + view + " WHERE B > 0";
            rs = conn.createStatement().executeQuery(query).unwrap(PhoenixResultSet.class);
            // assert query is against viewIndex
            assertEquals(viewIndex, rs.unwrap(PhoenixResultSet.class).getStatement().getQueryPlan()
                    .getTableRef().getTable().getName().getString());
            assertEquals(!useStats ? 11 : 1, rs.unwrap(PhoenixResultSet.class).getStatement()
                    .getQueryPlan().getScans().get(0).size());
        }
    }

    @Test
    public void testQueryingWithUseStatsForParallelizationOnOff() throws SQLException {
        testUseStatsForParallelizationOnSaltedTable(true, true);
        testUseStatsForParallelizationOnSaltedTable(true, false);
        testUseStatsForParallelizationOnSaltedTable(false, true);
        testUseStatsForParallelizationOnSaltedTable(false, false);
    }

    private void testUseStatsForParallelizationOnSaltedTable(boolean useStatsFlag, boolean salted)
            throws SQLException {
        String tableName = generateUniqueName();
        Connection conn = DriverManager.getConnection(getUrl());
        conn.createStatement().execute(
            "create table " + tableName + "(k varchar not null primary key, v varchar) "
                    + (salted ? " SALT_BUCKETS=2," : "") + " USE_STATS_FOR_PARALLELIZATION="
                    + useStatsFlag);
        conn.createStatement().execute("upsert into " + tableName + " values ('1', 'B')");
        conn.createStatement().execute("upsert into " + tableName + " values ('2', 'A')");
        conn.commit();
        String query = "SELECT V FROM " + tableName + " ORDER BY V";
        ResultSet rs = conn.createStatement().executeQuery(query);
        assertTrue(rs.next());
        assertEquals("A", rs.getString(1));
        assertTrue(rs.next());
        assertEquals("B", rs.getString(1));

        // Collect stats and make sure query still works correctly
        conn.createStatement().execute("UPDATE STATISTICS " + tableName);
        rs = conn.createStatement().executeQuery(query);
        assertTrue(rs.next());
        assertEquals("A", rs.getString(1));
        assertTrue(rs.next());
        assertEquals("B", rs.getString(1));
    }

    @Test
    public void testUseStatsForParallelizationProperyOnViewIndex() throws SQLException {
        String tableName = generateUniqueName();
        String viewName = generateUniqueName();
        String tenantViewName = generateUniqueName();
        String viewIndexName = generateUniqueName();
        boolean useStats = !DEFAULT_USE_STATS_FOR_PARALLELIZATION;
        try (Connection conn = DriverManager.getConnection(getUrl())) {
            conn.createStatement()
                    .execute("create table " + tableName
                            + "(tenantId CHAR(15) NOT NULL, pk1 integer NOT NULL, v varchar CONSTRAINT PK PRIMARY KEY "
                            + "(tenantId, pk1)) MULTI_TENANT=true");
            try (Connection tenantConn = getTenantConnection("tenant1")) {
                conn.createStatement().execute("CREATE VIEW " + viewName + " AS SELECT * FROM " + tableName);
                conn.createStatement().execute("CREATE INDEX " + viewIndexName + " on " + viewName + " (v) ");
                tenantConn.createStatement().execute("CREATE VIEW " + tenantViewName + " AS SELECT * FROM " + viewName);
                conn.createStatement()
                        .execute("ALTER TABLE " + tableName + " set USE_STATS_FOR_PARALLELIZATION=" + useStats);
                // changing a property on a base table is propagated to its view
                // if the view has not previously modified the property
                validatePropertyOnViewIndex(viewName, viewIndexName, useStats, conn, tenantConn);
            }
        }
    }

    private void validatePropertyOnViewIndex(String viewName, String viewIndexName, boolean useStats, Connection conn,
            Connection tenantConn) throws SQLException, TableNotFoundException {
        // fetch the latest view ptable
        PhoenixRuntime.getTableNoCache(tenantConn, viewName);
        PhoenixConnection phxConn = conn.unwrap(PhoenixConnection.class);
        PTable viewIndex = phxConn.getTable(new PTableKey(phxConn.getTenantId(), viewIndexName));
        assertEquals("USE_STATS_FOR_PARALLELIZATION property set incorrectly", useStats,
                PhoenixConfigurationUtil
                        .getStatsForParallelizationProp(tenantConn.unwrap(PhoenixConnection.class), viewIndex));
    }

}