/*
 * 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 org.apache.hadoop.hbase.KeepDeletedCells;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor;
import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
import org.apache.hadoop.hbase.client.TableDescriptor;
import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.schema.PName;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.util.MetaDataUtil;
import org.apache.phoenix.util.PhoenixRuntime;
import org.apache.phoenix.util.SchemaUtil;
import org.junit.Test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.apache.phoenix.query.QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES;
import static org.apache.phoenix.util.MetaDataUtil.SYNCED_DATA_TABLE_AND_INDEX_COL_FAM_PROPERTIES;
import static org.apache.phoenix.util.MetaDataUtil.VIEW_INDEX_TABLE_PREFIX;
import static org.apache.phoenix.util.UpgradeUtil.UPSERT_UPDATE_CACHE_FREQUENCY;
import static org.apache.phoenix.util.UpgradeUtil.syncTableAndIndexProperties;
import static org.apache.phoenix.util.UpgradeUtil.syncUpdateCacheFreqAllIndexes;
import static org.apache.phoenix.end2end.index.IndexMetadataIT.assertUpdateCacheFreq;

/**
 * Test properties that need to be kept in sync amongst all column families and indexes of a table
 */
public class PropertiesInSyncIT extends ParallelStatsDisabledIT {
    private static final String COL_FAM1 = "CF1";
    private static final String COL_FAM2 = "CF2";
    private static final String NEW_CF = "NEW_CF";
    private static final String DUMMY_PROP_VALUE = "dummy";
    private static final int INITIAL_TTL_VALUE = 700;
    private static final KeepDeletedCells INITIAL_KEEP_DELETED_CELLS_VALUE = KeepDeletedCells.TRUE;
    private static final int INITIAL_REPLICATION_SCOPE_VALUE = 1;
    private static final int INITIAL_UPDATE_CACHE_FREQUENCY = 100;
    private static final int INITIAL_UPDATE_CACHE_FREQUENCY_VIEWS = 900;
    private static final int MODIFIED_TTL_VALUE = INITIAL_TTL_VALUE + 300;
    private static final KeepDeletedCells MODIFIED_KEEP_DELETED_CELLS_VALUE =
            (INITIAL_KEEP_DELETED_CELLS_VALUE == KeepDeletedCells.TRUE) ? KeepDeletedCells.FALSE: KeepDeletedCells.TRUE;
    private static final int MODIFIED_REPLICATION_SCOPE_VALUE = (INITIAL_REPLICATION_SCOPE_VALUE == 1) ? 0 : 1;
    private static final int MODIFIED_UPDATE_CACHE_FREQUENCY = INITIAL_UPDATE_CACHE_FREQUENCY + 300;
    private static final int MODIFIED_UPDATE_CACHE_FREQUENCY_VIEWS = INITIAL_UPDATE_CACHE_FREQUENCY_VIEWS + 300;


    // Test that we disallow specifying synced properties to be set per column family when creating a table
    @Test
    public void testDisallowSyncedPropsToBeSetColFamSpecificCreateTable() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = generateUniqueName();
        for (String propName: SYNCED_DATA_TABLE_AND_INDEX_COL_FAM_PROPERTIES) {
            try {
                conn.createStatement().execute("create table " + tableName
                        + " (id INTEGER not null primary key, "
                        + COL_FAM1 + ".name varchar(10), " + COL_FAM2 + ".flag boolean) "
                        + COL_FAM1 + "." + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when setting synced property for a specific column family");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to set synced property for a specific column family",
                        SQLExceptionCode.COLUMN_FAMILY_NOT_ALLOWED_FOR_PROPERTY.getErrorCode(), sqlE.getErrorCode());
            }
        }
        conn.close();
    }

    // Test that all column families have the same value of synced properties when creating a table
    @Test
    public void testSyncedPropsAllColFamsCreateTable() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        verifyHBaseColumnFamilyProperties(tableName, conn, false, false);
        conn.close();
    }

    // Test that we disallow specifying synced properties to be set when creating an index on a physical table or a view
    @Test
    public void testDisallowSyncedPropsToBeSetCreateIndex() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        String localIndexName = tableName + "_LOCAL_IDX";
        String globalIndexName = tableName + "_GLOBAL_IDX";
        String viewName = "VIEW_" + tableName;
        conn.createStatement().execute("create view " + viewName
                + " (new_col SMALLINT) as select * from " + tableName + " where id > 1");
        for (String propName: SYNCED_DATA_TABLE_AND_INDEX_COL_FAM_PROPERTIES) {
            try {
                conn.createStatement().execute("create local index " + localIndexName
                        + " on " + tableName + "(name) "
                        + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when setting synced property for a local index");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to set synced property for a local index",
                        SQLExceptionCode.CANNOT_SET_OR_ALTER_PROPERTY_FOR_INDEX.getErrorCode(), sqlE.getErrorCode());
            }
            try {
                conn.createStatement().execute("create index " + globalIndexName
                        + " on " + tableName + "(flag) "
                        + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when setting synced property for a global index");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to set synced property for a global index",
                        SQLExceptionCode.CANNOT_SET_OR_ALTER_PROPERTY_FOR_INDEX.getErrorCode(), sqlE.getErrorCode());
            }
            try {
                conn.createStatement().execute("create index view_index"
                        + " on " + viewName + " (flag)" + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when setting synced property for a view index");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to set synced property for a view index",
                        SQLExceptionCode.CANNOT_SET_OR_ALTER_PROPERTY_FOR_INDEX.getErrorCode(), sqlE.getErrorCode());
            }
        }
        conn.close();
    }

    // Test that indexes have the same value of synced properties as their base table
    @Test
    public void testSyncedPropsBaseTableCreateIndex() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        createIndexTable(conn, tableName, PTable.IndexType.LOCAL).getSecond();
        String globalIndexName = createIndexTable(conn, tableName, PTable.IndexType.GLOBAL).getSecond();

        // We pass the base table as the physical HBase table since our check includes checking the local index column family too
        verifyHBaseColumnFamilyProperties(tableName, conn, false, false);
        verifyHBaseColumnFamilyProperties(globalIndexName, conn, false, false);
        conn.close();
    }

    // Test that the physical view index table has the same value of synced properties as its base table
    @Test
    public void testSyncedPropsBaseTableCreateViewIndex() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        String viewIndexName = createIndexTable(conn, tableName, null).getSecond();

        verifyHBaseColumnFamilyProperties(tableName, conn, false, false);
        verifyHBaseColumnFamilyProperties(viewIndexName, conn, false, false);
        conn.close();
    }

    // Test that we disallow specifying synced properties to be set per column family when altering a table
    @Test
    public void testDisallowSyncedPropsToBeSetColFamSpecificAlterTable() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        StringBuilder alterAllSyncedPropsString = new StringBuilder();
        String modPropString = COL_FAM1 + ".%s=" + DUMMY_PROP_VALUE + ",";
        for (String propName: SYNCED_DATA_TABLE_AND_INDEX_COL_FAM_PROPERTIES) {
            try {
                conn.createStatement().execute("alter table " + tableName
                        + " set " + COL_FAM1 + "." + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when altering synced property for a specific column family");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to alter synced property for a specific column family",
                        SQLExceptionCode.COLUMN_FAMILY_NOT_ALLOWED_FOR_PROPERTY.getErrorCode(), sqlE.getErrorCode());
            }
            alterAllSyncedPropsString.append(String.format(modPropString, propName));
        }

        // Test the same when we try to set all of these properties at once
        try {
            conn.createStatement().execute("alter table " + tableName + " set "
                    + alterAllSyncedPropsString.substring(0, alterAllSyncedPropsString.length() - 1));
            fail("Should fail with SQLException when altering synced properties for a specific column family");
        } catch (SQLException sqlE) {
            assertEquals("Should fail to alter synced properties for a specific column family",
                    SQLExceptionCode.COLUMN_FAMILY_NOT_ALLOWED_FOR_PROPERTY.getErrorCode(), sqlE.getErrorCode());
        }
        conn.close();
    }

    // Test that any alteration of the synced properties gets propagated to all indexes and the physical view index table
    @Test
    public void testAlterSyncedPropsPropagateToAllIndexesAndViewIndex() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        Set<String> tablesToCheck = new HashSet<>();
        tablesToCheck.add(tableName);
        for (int i=0; i<2; i++) {
            tablesToCheck.add(createIndexTable(conn, tableName, PTable.IndexType.LOCAL).getSecond());
            tablesToCheck.add(createIndexTable(conn, tableName, PTable.IndexType.GLOBAL).getSecond());
        }
        // Create a view and view index
        tablesToCheck.add(createIndexTable(conn, tableName, null).getSecond());

        // Now alter the base table's properties. This should get propagated to all index tables
        conn.createStatement().execute("alter table " + tableName + " set TTL=" + MODIFIED_TTL_VALUE
                + ",KEEP_DELETED_CELLS=" + MODIFIED_KEEP_DELETED_CELLS_VALUE
                + ",REPLICATION_SCOPE=" + MODIFIED_REPLICATION_SCOPE_VALUE);

        for (String table: tablesToCheck) {
            verifyHBaseColumnFamilyProperties(table, conn, true, false);
        }

        // Any indexes created henceforth should have the modified properties
        String newGlobalIndex = createIndexTable(conn, tableName, PTable.IndexType.GLOBAL).getSecond();
        String newViewIndex = createIndexTable(conn, tableName, null).getSecond();
        verifyHBaseColumnFamilyProperties(newGlobalIndex, conn, true, false);
        verifyHBaseColumnFamilyProperties(newViewIndex, conn, true, false);
        conn.close();
    }

    // Test that any if we add a column family to a base table, it gets the synced properties
    @Test
    public void testAlterTableAddColumnFamilyGetsSyncedProps() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);

        // Test that we are not allowed to set any property to be kept in sync, specific to the new column family to be added
        for (String propName: SYNCED_DATA_TABLE_AND_INDEX_COL_FAM_PROPERTIES) {
            try {
                conn.createStatement().execute(
                        "alter table " + tableName + " add " + NEW_CF + ".new_column varchar(2) "
                                + NEW_CF + "." + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when altering synced property for a specific column family when adding a column");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to alter synced property for a specific column family when adding a column",
                        SQLExceptionCode.COLUMN_FAMILY_NOT_ALLOWED_FOR_PROPERTY.getErrorCode(), sqlE.getErrorCode());
            }
        }

        // Test that when we add a new column (belonging to a new column family) and set any property that should be
        // in sync, then the property is modified for all existing column families of the base table and its indexes
        Set<String> tablesToCheck = new HashSet<>();
        tablesToCheck.add(tableName);
        for (int i=0; i<2; i++) {
            tablesToCheck.add(createIndexTable(conn, tableName, PTable.IndexType.LOCAL).getSecond());
            tablesToCheck.add(createIndexTable(conn, tableName, PTable.IndexType.GLOBAL).getSecond());
        }
        // Create a view and view index
        tablesToCheck.add(createIndexTable(conn, tableName, null).getSecond());

        // Now add a new column family while simultaneously modifying properties to be kept in sync,
        // as well as a property which does not need to be kept in sync. Properties to be kept in sync
        // should get propagated to all index tables and already existing column families
        conn.createStatement().execute(
                "alter table " + tableName + " add " + NEW_CF + ".new_column varchar(2) "
                + "KEEP_DELETED_CELLS=" + MODIFIED_KEEP_DELETED_CELLS_VALUE
                + ",REPLICATION_SCOPE=" + MODIFIED_REPLICATION_SCOPE_VALUE
                + ",BLOCKSIZE=300000");

        for (String table: tablesToCheck) {
            verifyHBaseColumnFamilyProperties(table, conn, true, true);
        }
        try (Admin admin = conn.unwrap(PhoenixConnection.class).getQueryServices().getAdmin()) {
            ColumnFamilyDescriptor[] columnFamilies = admin.getDescriptor(TableName.valueOf(tableName))
                    .getColumnFamilies();
            for (ColumnFamilyDescriptor cfd: columnFamilies) {
                if (cfd.getNameAsString().equals(NEW_CF)) {
                    assertEquals("Newly added column fmaily should have updated property",
                            300000, cfd.getBlocksize());
                } else {
                    assertEquals("Existing column families should have default value for property",
                            ColumnFamilyDescriptorBuilder.DEFAULT_BLOCKSIZE, cfd.getBlocksize());
                }
            }
        }
        conn.close();
    }

    // Test that we disallow altering a synced property for a global index table
    @Test
    public void testDisallowAlterGlobalIndexTable() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());
        String tableName = createBaseTableWithProps(conn);
        String globalIndexName = createIndexTable(conn, tableName, PTable.IndexType.GLOBAL).getSecond();
        for (String propName: SYNCED_DATA_TABLE_AND_INDEX_COL_FAM_PROPERTIES) {
            try {
                conn.createStatement().execute("alter table " + globalIndexName + " set "
                + propName + "=" + DUMMY_PROP_VALUE);
                fail("Should fail with SQLException when altering synced property for a global index");
            } catch (SQLException sqlE) {
                assertEquals("Should fail to alter synced property for a global index",
                        SQLExceptionCode.CANNOT_SET_OR_ALTER_PROPERTY_FOR_INDEX.getErrorCode(), sqlE.getErrorCode());
            }
        }
        conn.close();
    }

    // Test the upgrade code path for old client to new phoenix server cases in which the client
    // may have tables which have column families and indexes whose properties are out of sync
    @Test
    public void testOldClientSyncPropsUpgradePath() throws Exception {
        Connection conn = DriverManager.getConnection(getUrl(), new Properties());

        String baseTableName = createBaseTableWithProps(conn);
        String baseTableName1 = createBaseTableWithProps(conn);
        Set<String> createdTables = new HashSet<>();
        createdTables.add(baseTableName);
        createdTables.add(baseTableName1);
        // Create different indexes on the base table
        for (int i=0; i<2; i++) {
            createdTables.add(createIndexTable(conn, baseTableName, PTable.IndexType.GLOBAL).getSecond());
            createdTables.add(createIndexTable(conn, baseTableName, PTable.IndexType.LOCAL).getSecond());
            createdTables.add(createIndexTable(conn, baseTableName, null).getSecond());
            createdTables.add(createIndexTable(conn, baseTableName1, PTable.IndexType.GLOBAL).getSecond());
            createdTables.add(createIndexTable(conn, baseTableName1, PTable.IndexType.LOCAL).getSecond());
            createdTables.add(createIndexTable(conn, baseTableName1, null).getSecond());
        }
        for (String t: createdTables) {
            verifyHBaseColumnFamilyProperties(t, conn, false, false);
        }

        try (Admin admin = conn.unwrap(PhoenixConnection.class).getQueryServices().getAdmin()) {
            for (String tableName: createdTables) {
                final TableDescriptor tableDescriptor;
                final ColumnFamilyDescriptor defaultCF;
                if (MetaDataUtil.isViewIndex(tableName)) {
                    // We won't be able to get the PTable for a view index table
                    tableDescriptor = conn.unwrap(PhoenixConnection.class).getQueryServices()
                            .getTableDescriptor(Bytes.toBytes(tableName));
                    defaultCF = tableDescriptor.getColumnFamily(DEFAULT_COLUMN_FAMILY_BYTES);
                } else {
                    PTable table = PhoenixRuntime.getTable(conn, tableName);
                    tableDescriptor = conn.unwrap(PhoenixConnection.class).getQueryServices()
                            .getTableDescriptor(table.getPhysicalName().getBytes());
                    defaultCF = tableDescriptor.getColumnFamily(SchemaUtil.getEmptyColumnFamily(table));
                }

                TableDescriptorBuilder tableDescBuilder = TableDescriptorBuilder.newBuilder(tableDescriptor);
                if (tableName.equals(baseTableName) || tableName.equals(baseTableName1)) {
                    for (ColumnFamilyDescriptor cfd: tableDescriptor.getColumnFamilies()) {
                        // Modify all column families except the default column family for the base tables
                        if (!cfd.equals(defaultCF)) {
                            ColumnFamilyDescriptorBuilder cfdb = ColumnFamilyDescriptorBuilder.newBuilder(cfd);
                            modifySyncedPropsForCF(cfdb);
                            tableDescBuilder.modifyColumnFamily(cfdb.build());
                        }
                    }
                } else {
                    for (ColumnFamilyDescriptor cfd: tableDescriptor.getColumnFamilies()) {
                        // Modify all column families for other tables
                        ColumnFamilyDescriptorBuilder cfdb = ColumnFamilyDescriptorBuilder.newBuilder(cfd);
                        modifySyncedPropsForCF(cfdb);
                        tableDescBuilder.modifyColumnFamily(cfdb.build());
                    }
                }
                admin.modifyTable(tableDescBuilder.build());
            }
        }
        // Now synchronize required properties and verify HBase metadata property values
        PhoenixConnection upgradeConn = conn.unwrap(PhoenixConnection.class);
        // Simulate an upgrade by setting the upgrade flag
        upgradeConn.setRunningUpgrade(true);
        syncTableAndIndexProperties(upgradeConn, upgradeConn.getQueryServices().getAdmin());
        for (String t: createdTables) {
            verifyHBaseColumnFamilyProperties(t, conn, false, false);
        }
        conn.close();
    }

    @Test
    public void testOldClientSyncUpdateCacheFreqUpgradePath() throws Exception {
        PTable base, index;
        String baseTableName, viewName, viewName2;
        Map<String, Set<String>> createdTablesAndViews = new HashMap<>();

        try (Connection conn = DriverManager.getConnection(getUrl(), new Properties())) {
            baseTableName = createBaseTableWithProps(conn);
            createdTablesAndViews.put(baseTableName, new HashSet<>());
            Set<String> indexes = createdTablesAndViews.get(baseTableName);
            indexes.add(createIndexTable(conn, baseTableName, PTable.IndexType.GLOBAL).getSecond());
            indexes.add(createIndexTable(conn, baseTableName, PTable.IndexType.LOCAL).getFirst());

            viewName = createViewOnBaseTableOrView(conn, baseTableName);
            createdTablesAndViews.put(viewName, new HashSet<>());
            indexes = createdTablesAndViews.get(viewName);
            indexes.add(createIndexTable(conn, viewName, PTable.IndexType.GLOBAL).getSecond());

            viewName2 = createViewOnBaseTableOrView(conn, viewName);
            createdTablesAndViews.put(viewName2, new HashSet<>());
            indexes = createdTablesAndViews.get(viewName2);
            indexes.add(createIndexTable(conn, viewName2, PTable.IndexType.LOCAL).getFirst());

            // Intentionally make UPDATE_CACHE_FREQUENCY out of sync for indexes
            PreparedStatement stmt = conn.prepareStatement(UPSERT_UPDATE_CACHE_FREQUENCY);
            for (String tableOrViewName : createdTablesAndViews.keySet()) {
                base = PhoenixRuntime.getTable(conn, tableOrViewName);
                for (String indexTableName : createdTablesAndViews.get(tableOrViewName)) {
                    index = PhoenixRuntime.getTable(conn, indexTableName);
                    PName tenantId = index.getTenantId();
                    stmt.setString(1, tenantId == null ? null : tenantId.getString());
                    stmt.setString(2, index.getSchemaName().getString());
                    stmt.setString(3, index.getTableName().getString());
                    stmt.setLong(4, base.getType() == PTableType.TABLE ?
                            MODIFIED_UPDATE_CACHE_FREQUENCY : MODIFIED_UPDATE_CACHE_FREQUENCY_VIEWS);
                    stmt.addBatch();
                }
            }
            stmt.executeBatch();
            conn.commit();

            // Clear the server-side cache so that we get the latest built PTables
            conn.unwrap(PhoenixConnection.class).getQueryServices().clearCache();
            // Verify that the modified values are reflected
            for (String tableOrViewName : createdTablesAndViews.keySet()) {
                assertUpdateCacheFreq(conn, tableOrViewName, baseTableName.equals(tableOrViewName) ?
                        INITIAL_UPDATE_CACHE_FREQUENCY : INITIAL_UPDATE_CACHE_FREQUENCY_VIEWS);
                for (String indexName : createdTablesAndViews.get(tableOrViewName)) {
                    assertUpdateCacheFreq(conn, indexName, baseTableName.equals(tableOrViewName) ?
                            MODIFIED_UPDATE_CACHE_FREQUENCY : MODIFIED_UPDATE_CACHE_FREQUENCY_VIEWS);
                }
            }

            PhoenixConnection upgradeConn = conn.unwrap(PhoenixConnection.class);
            upgradeConn.setRunningUpgrade(true);
            syncUpdateCacheFreqAllIndexes(upgradeConn,
                    PhoenixRuntime.getTableNoCache(conn, baseTableName));

            conn.unwrap(PhoenixConnection.class).getQueryServices().clearCache();
            // Verify that indexes have the synced values for UPDATE_CACHE_FREQUENCY
            for (String tableOrViewName : createdTablesAndViews.keySet()) {
                long expectedVal = baseTableName.equals(tableOrViewName) ?
                        INITIAL_UPDATE_CACHE_FREQUENCY : INITIAL_UPDATE_CACHE_FREQUENCY_VIEWS;
                assertUpdateCacheFreq(conn, tableOrViewName, expectedVal);
                for (String indexOnTableOrView : createdTablesAndViews.get(tableOrViewName)) {
                    assertUpdateCacheFreq(conn, indexOnTableOrView, expectedVal);
                }
            }
        }
    }

    /**
     * Helper method to modify the synced properties for a column family descriptor
     * @param cfdb The column family descriptor builder object
     * @throws SQLException
     */
    private void modifySyncedPropsForCF(ColumnFamilyDescriptorBuilder cfdb) throws SQLException {
        cfdb.setTimeToLive(MODIFIED_TTL_VALUE);
        cfdb.setKeepDeletedCells(MODIFIED_KEEP_DELETED_CELLS_VALUE);
        cfdb.setScope(MODIFIED_REPLICATION_SCOPE_VALUE);
    }

    /**
     * Helper method to create or alter a base table with specific values set for properties to be kept in sync
     * @param conn Phoenix connection
     * @return Name of the HBase table created
     * @throws SQLException
     */
    private String createBaseTableWithProps(Connection conn) throws SQLException {
        String tableName = generateUniqueName();
        conn.createStatement().execute("create table " + tableName
                + " (id INTEGER not null primary key, type varchar(5), "
                + COL_FAM1 + ".name varchar(10), " + COL_FAM2 + ".flag boolean) "
                + "TTL=" + INITIAL_TTL_VALUE + ",KEEP_DELETED_CELLS=" + INITIAL_KEEP_DELETED_CELLS_VALUE
                + ",REPLICATION_SCOPE=" + INITIAL_REPLICATION_SCOPE_VALUE
                + ",UPDATE_CACHE_FREQUENCY=" + INITIAL_UPDATE_CACHE_FREQUENCY);
        return tableName;
    }

    /**
     * Helper method to create an index table on a base table.
     * @param conn Phoenix connection
     * @param baseTableName Name of the HBase base table on which to create an index
     * @param indexType LOCAL, GLOBAL or if we pass in null as the indexType,
     *                 we create a view and an index on that view for the given base table
     * @return A pair consisting of the index name and the name of the physical HBase table
     * corresponding to the index created
     * @throws SQLException
     */
    private Pair<String,String> createIndexTable(Connection conn, String baseTableName,
            PTable.IndexType indexType) throws SQLException {
        // Create a view on top of the base table and then an index on that view
        if (indexType == null) {
            String viewName = createViewOnBaseTableOrView(conn, baseTableName);
            String viewIndexName = VIEW_INDEX_TABLE_PREFIX + baseTableName;
            conn.createStatement().execute("create index view_index_" + generateUniqueName()
                    + " on " + viewName + " (flag)");
            return new Pair<>(viewIndexName, viewIndexName);
        }
        switch(indexType) {
        case LOCAL:
            String localIndexName = baseTableName + "_LOCAL_" + generateUniqueName();
            conn.createStatement().execute(
                    "create local index " + localIndexName + " on " + baseTableName + "(flag)");
            return new Pair<>(localIndexName, baseTableName);
        case GLOBAL:
            String globalIndexName = baseTableName + "_GLOBAL_" + generateUniqueName();
            conn.createStatement()
                    .execute("create index " + globalIndexName + " on " + baseTableName + "(name)");
            return new Pair<>(globalIndexName, globalIndexName);
        default:
            return new Pair<>(baseTableName, baseTableName);
        }
    }

    private String createViewOnBaseTableOrView(Connection conn, String baseTableOrView) throws SQLException {
        String viewName = "VIEW_" + baseTableOrView + "_" + generateUniqueName();
        conn.createStatement().execute("create view " + viewName
                + " (" + generateUniqueName() + " SMALLINT) as select * from "
                + baseTableOrView + " where id > 1 UPDATE_CACHE_FREQUENCY="
                + INITIAL_UPDATE_CACHE_FREQUENCY_VIEWS);
        return viewName;
    }

    /**
     * Helper method to verify HBase column family properties
     * @param tableName Physical HBase table whose properties are to be verified
     * @param conn Phoenix connection
     * @param propModified true if we have altered any of the properties to be kept in sync, false otherwise
     * @param ignoreTTL We cannot modfiy a table level property when adding a column, so in those cases,
     *                 ignore the check for TTL modification
     * @throws Exception
     */
    private void verifyHBaseColumnFamilyProperties(String tableName, Connection conn, boolean propModified,
            boolean ignoreTTL) throws Exception {
        final int expectedTTL = propModified ? MODIFIED_TTL_VALUE:INITIAL_TTL_VALUE;
        final KeepDeletedCells expectedKeepDeletedCells = propModified ? MODIFIED_KEEP_DELETED_CELLS_VALUE: INITIAL_KEEP_DELETED_CELLS_VALUE;
        final int expectedReplicationScope = propModified ? MODIFIED_REPLICATION_SCOPE_VALUE:INITIAL_REPLICATION_SCOPE_VALUE;

        try (Admin admin = conn.unwrap(PhoenixConnection.class).getQueryServices().getAdmin()) {
            // Note that this includes the local index column family as well
            ColumnFamilyDescriptor[] columnFamilies = admin.getDescriptor(TableName.valueOf(tableName))
                    .getColumnFamilies();
            for (ColumnFamilyDescriptor cfd: columnFamilies) {
                if (!ignoreTTL) {
                    assertEquals("Mismatch in TTL", expectedTTL, cfd.getTimeToLive());
                }
                assertEquals("Mismatch in KEEP_DELETED_CELLS", expectedKeepDeletedCells, cfd.getKeepDeletedCells());
                assertEquals("Mismatch in REPLICATION_SCOPE", expectedReplicationScope, cfd.getScope());
            }
        }
    }
}