/* * 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.index; import org.apache.hadoop.hbase.Cell; import org.apache.hadoop.hbase.CellUtil; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.client.Delete; import org.apache.hadoop.hbase.client.Mutation; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.util.Bytes; import org.apache.phoenix.coprocessor.IndexRebuildRegionScanner; import org.apache.phoenix.hbase.index.IndexRegionObserver; import org.apache.phoenix.jdbc.PhoenixConnection; import org.apache.phoenix.query.BaseConnectionlessQueryTest; import org.apache.phoenix.query.QueryConstants; import org.apache.phoenix.schema.PTable; import org.apache.phoenix.schema.PTableKey; import org.apache.phoenix.util.SchemaUtil; import org.junit.Assert; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.DriverManager; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static org.junit.Assert.assertEquals; public class PrepareIndexMutationsForRebuildTest extends BaseConnectionlessQueryTest { private static String ROW_KEY = "k1"; private static String TABLE_NAME = "dataTable"; private static String INDEX_NAME = "idx"; class SetupInfo { public IndexMaintainer indexMaintainer; public PTable pDataTable; } /** * Get the index maintainer and phoenix table definition of data table. * @param tableName * @param indexName * @param columns * @param indexColumns * @param pk * @param includeColumns * @return * @throws Exception */ private SetupInfo setup(String tableName, String indexName, String columns, String indexColumns, String pk, String includeColumns) throws Exception { try(Connection conn = DriverManager.getConnection(getUrl())) { String fullTableName = SchemaUtil.getTableName(SchemaUtil.normalizeIdentifier(""), SchemaUtil.normalizeIdentifier(tableName)); String fullIndexName = SchemaUtil.getTableName(SchemaUtil.normalizeIdentifier(""), SchemaUtil.normalizeIdentifier(indexName)); // construct the data table and index based from the parameters String str1 = String.format("CREATE TABLE %1$s (%2$s CONSTRAINT pk PRIMARY KEY (%3$s)) COLUMN_ENCODED_BYTES=0", fullTableName, columns, pk); conn.createStatement().execute(str1); String str2 = String.format("CREATE INDEX %1$s ON %2$s (%3$s)", fullIndexName, fullTableName, indexColumns); if (!includeColumns.isEmpty()) str2 += " INCLUDE (" + includeColumns + ")"; conn.createStatement().execute(str2); // Get the data table, index table and index maintainer reference from the client's ConnectionQueryServiceImpl // In this way, we don't need to setup a local cluster. PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class); PTable pIndexTable = pconn.getTable(new PTableKey(pconn.getTenantId(), fullIndexName)); PTable pDataTable = pconn.getTable(new PTableKey(pconn.getTenantId(), fullTableName)); IndexMaintainer im = pIndexTable.getIndexMaintainer(pDataTable, pconn); SetupInfo info = new SetupInfo(); info.indexMaintainer = im; info.pDataTable = pDataTable; return info; } } /** * Simulate one put mutation on the indexed column * @throws Exception */ @Test public void testSinglePutOnIndexColumn() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR", "C1", "ROW_KEY", ""); // insert a row Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, null); // Expect one row of index with row key "v1_k1" Put idxPut1 = new Put(generateIndexRowKey("v1")); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); assertEqualMutationList(Arrays.asList((Mutation)idxPut1), actualIndexMutations); } /** * Simulate one put mutation on the non-indexed column * @throws Exception */ @Test public void testSinglePutOnNonIndexColumn() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR", "C1", "ROW_KEY", ""); Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, null); // Expect one row of index with row key "_k1", as indexed column C1 is nullable. Put idxPut1 = new Put(generateIndexRowKey(null)); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); assertEqualMutationList(Arrays.asList((Mutation)idxPut1), actualIndexMutations); } /** * Simulate the column delete on the index column * @throws Exception */ @Test public void testDelOnIndexColumn() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR", "C1", "ROW_KEY", ""); // insert the row for deletion Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); // only delete the value of column C1 Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY)); addCellToDelMutation(dataDel, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 2, KeyValue.Type.DeleteColumn); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, dataDel); List<Mutation> expectedIndexMutation = new ArrayList<>(); // generate the index row key "v1_k1" byte[] idxKeyBytes = generateIndexRowKey("v1"); Put idxPut1 = new Put(idxKeyBytes); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); expectedIndexMutation.add(idxPut1); // generate the index row key "_k1" Put idxPut2 = new Put(generateIndexRowKey(null)); addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2); expectedIndexMutation.add(idxPut2); // This deletion is to remove the row added by the idxPut1, as idxPut2 has different row key as idxPut1. // Otherwise the row "v1_k1" will still be shown in the scan result Delete idxDel = new Delete(idxKeyBytes); addCellToDelMutation(idxDel, QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, null, 2, KeyValue.Type.DeleteFamily); expectedIndexMutation.add(idxDel); assertEqualMutationList(expectedIndexMutation, actualIndexMutations); } /** * Simulate the column delete on the non-indexed column * @throws Exception */ @Test public void testDelOnNonIndexColumn() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR", "C1", "ROW_KEY", ""); // insert the row for deletion Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); // delete the value of column C2 Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY)); addCellToDelMutation(dataDel, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 2, KeyValue.Type.DeleteColumn); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, dataDel); List<Mutation> expectedIndexMutations = new ArrayList<>(); byte[] idxKeyBytes = generateIndexRowKey("v1"); // idxPut1 is the corresponding index mutation of dataPut Put idxPut1 = new Put(idxKeyBytes); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); expectedIndexMutations.add(idxPut1); // idxPut2 is required to update the timestamp, so the index row will have the same life time as its corresponding data row. // No delete mutation is expected on index table, as data mutation happens only on non-indexed column. Put idxPut2 = new Put(idxKeyBytes); addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2); expectedIndexMutations.add(idxPut2); assertEqualMutationList(expectedIndexMutations, actualIndexMutations); } /** * Simulate the data deletion of all version on the indexed row * @throws Exception */ @Test public void testDeleteAllVersions() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR", "C1", "ROW_KEY", ""); // insert two versions for a single row Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 2, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 2); // DeleteFamily will delete all versions of the columns in that family // Since C1 is the only column of the default column family, so deleting the default family removes all version // of column C1 Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY)); addCellToDelMutation(dataDel, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), null, 3, KeyValue.Type.DeleteFamily); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, dataDel); List<Mutation> expectedIndexMutations = new ArrayList<>(); byte[] idxKeyBytes1 = generateIndexRowKey("v1"); byte[] idxKeyBytes2 = generateIndexRowKey("v2"); // idxPut1 and idxPut2 are generated by two versions in dataPut Put idxPut1 = new Put(idxKeyBytes1); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); expectedIndexMutations.add(idxPut1); Put idxPut2 = new Put(idxKeyBytes2); addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2); expectedIndexMutations.add(idxPut2); // idxDel1 is required to remove the row key "v1_k1" which is added by idxPut1. // The ts of idxDel1 is same as idxPut2, because it is a result of idxPut2. // Since C1 is the only index column, so it is translated to DeleteFamily mutation. Delete idxDel1 = new Delete(idxKeyBytes1); addCellToDelMutation(idxDel1, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), null, 2, KeyValue.Type.DeleteFamily); expectedIndexMutations.add(idxDel1); // idxDel2 is corresponding index mutation of dataDel Delete idxDel2 = new Delete(idxKeyBytes2); addCellToDelMutation(idxDel2, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), null, 3, KeyValue.Type.DeleteFamily); expectedIndexMutations.add(idxDel2); assertEqualMutationList(expectedIndexMutations, actualIndexMutations); } // Simulate the put and delete mutation with the same time stamp on the index @Test public void testPutDeleteOnSameTimeStamp() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR", "C1", "ROW_KEY", ""); // insert a row Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable,1); // delete column of C1 from the inserted row Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY)); addCellToDelMutation(dataDel, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, KeyValue.Type.DeleteColumn); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, dataDel); List<Mutation> expectedIndexMutations = new ArrayList<>(); // The dataDel will be applied on top of dataPut when we replay them for index rebuild, when they have the same time stamp. // idxPut1 is expected as in data table we still see the row of k1 with empty C1, so we need a row in index table with row key "_k1" Put idxPut1 = new Put(generateIndexRowKey(null)); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); expectedIndexMutations.add(idxPut1); assertEqualMutationList(Arrays.asList((Mutation)idxPut1), actualIndexMutations); } // Simulate the put and delete mutation on the covered column of data table @Test public void testCoveredIndexColumns() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR", "C1", "ROW_KEY", "C2"); Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY)); addCellToDelMutation(dataDel, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C1"), 2, KeyValue.Type.DeleteColumn); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, dataDel); List<Mutation> expectedIndexMutations = new ArrayList<>(); byte[] idxKeyBytes = generateIndexRowKey("v1"); // idxPut1 is generated corresponding to dataPut. // The column "0:C2" is generated from data table column family and column name, its family name is still default family name of index table Put idxPut1 = new Put(idxKeyBytes); addCellToPutMutation(idxPut1, QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, Bytes.toBytes("0:C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); expectedIndexMutations.add(idxPut1); // idxKey2 is required by dataDel, as dataDel change the corresponding row key of index table List<Byte> idxKey2 = new ArrayList<>(); idxKey2.add(QueryConstants.SEPARATOR_BYTE); idxKey2.addAll(com.google.common.primitives.Bytes.asList(Bytes.toBytes(ROW_KEY))); byte[] idxKeyBytes2 = com.google.common.primitives.Bytes.toArray(idxKey2); Put idxPut2 = new Put(idxKeyBytes2); addCellToPutMutation(idxPut2, QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, Bytes.toBytes("0:C2"), 2, Bytes.toBytes("v2")); addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2); expectedIndexMutations.add(idxPut2); // idxDel is required to invalid the index row "v1_k1", dataDel removed the value of indexed column Delete idxDel = new Delete(idxKeyBytes); addCellToDelMutation(idxDel, QueryConstants.DEFAULT_COLUMN_FAMILY_BYTES, null, 2, KeyValue.Type.DeleteFamily); expectedIndexMutations.add(idxDel); assertEqualMutationList(expectedIndexMutations, actualIndexMutations); } // Simulate the scenario that index column, and covered column belong to different column families @Test public void testForMultipleFamilies() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, CF1.C1 VARCHAR, CF2.C2 VARCHAR", //define C1 and C2 with different families "CF1.C1", "ROW_KEY", "CF2.C2"); // insert a row to the data table Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, Bytes.toBytes("CF1"), Bytes.toBytes("C1"), 1, Bytes.toBytes("v1")); addCellToPutMutation(dataPut, Bytes.toBytes("CF2"), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); // delete the indexed column CF1:C1 Delete dataDel = new Delete(Bytes.toBytes(ROW_KEY)); addCellToDelMutation(dataDel, Bytes.toBytes("CF1"), Bytes.toBytes("C1"), 2, KeyValue.Type.DeleteColumn); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, dataDel); List<Mutation> expectedIndexMutation = new ArrayList<>(); byte[] idxKeyBytes = generateIndexRowKey("v1"); // index table will use the family name of the first covered column, which is CF2 here. Put idxPut1 = new Put(idxKeyBytes); addCellToPutMutation(idxPut1, Bytes.toBytes("CF2"), Bytes.toBytes("CF2:C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); expectedIndexMutation.add(idxPut1); // idxPut2 and idxDel are the result of dataDel // idxPut2 is to create the index row "_k1", idxDel is to invalid the index row "v1_k1". Put idxPut2 = new Put(generateIndexRowKey(null)); addCellToPutMutation(idxPut2, Bytes.toBytes("CF2"), Bytes.toBytes("CF2:C2"), 2, Bytes.toBytes("v2")); addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2); expectedIndexMutation.add(idxPut2); Delete idxDel = new Delete(idxKeyBytes); addCellToDelMutation(idxDel, Bytes.toBytes("CF2"), null, 2, KeyValue.Type.DeleteFamily); expectedIndexMutation.add(idxDel); assertEqualMutationList(expectedIndexMutation, actualIndexMutations); } // Simulate two data put with the same value but different time stamp. // We expect to see 2 index mutations with same value but different time stamps. @Test public void testSameTypeOfMutationWithSameValueButDifferentTimeStamp() throws Exception { SetupInfo info = setup(TABLE_NAME, INDEX_NAME, "ROW_KEY VARCHAR, C1 VARCHAR, C2 VARCHAR", "C1", "ROW_KEY", ""); Put dataPut = new Put(Bytes.toBytes(ROW_KEY)); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v2")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 1); addCellToPutMutation(dataPut, info.indexMaintainer.getEmptyKeyValueFamily().copyBytesIfNecessary(), Bytes.toBytes("C2"), 1, Bytes.toBytes("v3")); addEmptyColumnToDataPutMutation(dataPut, info.pDataTable, 2); List<Mutation> actualIndexMutations = IndexRebuildRegionScanner.prepareIndexMutationsForRebuild(info.indexMaintainer, dataPut, null); byte[] idxKeyBytes = generateIndexRowKey(null); // idxPut1 and idxPut2 have same value but different time stamp Put idxPut1 = new Put(idxKeyBytes); addEmptyColumnToIndexPutMutation(idxPut1, info.indexMaintainer, 1); Put idxPut2 = new Put(idxKeyBytes); addEmptyColumnToIndexPutMutation(idxPut2, info.indexMaintainer, 2); assertEqualMutationList(Arrays.asList((Mutation)idxPut1, (Mutation)idxPut2), actualIndexMutations); } /** * Generate the row key for index table by the value of indexed column * @param indexVal * @return */ byte[] generateIndexRowKey(String indexVal) { List<Byte> idxKey = new ArrayList<>(); if (indexVal != null && !indexVal.isEmpty()) idxKey.addAll(com.google.common.primitives.Bytes.asList(Bytes.toBytes(indexVal))); idxKey.add(QueryConstants.SEPARATOR_BYTE); idxKey.addAll(com.google.common.primitives.Bytes.asList(Bytes.toBytes(ROW_KEY))); return com.google.common.primitives.Bytes.toArray(idxKey); } void addCellToPutMutation(Put put, byte[] family, byte[] column, long ts, byte[] value) throws Exception { byte[] rowKey = put.getRow(); Cell cell = CellUtil.createCell(rowKey, family, column, ts, KeyValue.Type.Put.getCode(), value); put.add(cell); } void addCellToDelMutation(Delete del, byte[] family, byte[] column, long ts, KeyValue.Type type) throws Exception { byte[] rowKey = del.getRow(); Cell cell = CellUtil.createCell(rowKey, family, column, ts, type.getCode(), null); del.addDeleteMarker(cell); } /** * Add Empty column to the existing data put mutation * @param put * @param ptable * @param ts * @throws Exception */ void addEmptyColumnToDataPutMutation(Put put, PTable ptable, long ts) throws Exception { addCellToPutMutation(put, SchemaUtil.getEmptyColumnFamily(ptable), QueryConstants.EMPTY_COLUMN_BYTES, ts, QueryConstants.EMPTY_COLUMN_VALUE_BYTES); } /** * Add the verified flag to the existing index put mutation * @param put * @param im * @param ts * @throws Exception */ void addEmptyColumnToIndexPutMutation(Put put, IndexMaintainer im, long ts) throws Exception { addCellToPutMutation(put, im.getEmptyKeyValueFamily().copyBytesIfNecessary(), QueryConstants.EMPTY_COLUMN_BYTES, ts, IndexRegionObserver.VERIFIED_BYTES); } /** * Compare two mutation lists without worrying about the order of the mutations in the lists * @param expectedMutations * @param actualMutations */ void assertEqualMutationList(List<Mutation> expectedMutations, List<Mutation> actualMutations) { assertEquals(expectedMutations.size(), actualMutations.size()); for (Mutation expected : expectedMutations) { boolean found = false; for (Mutation actual: actualMutations) { if (isEqualMutation(expected, actual)) { actualMutations.remove(actual); found = true; break; } } if (!found) Assert.fail(String.format("Cannot find mutation:%s", expected)); } } /** * Compare two mutations without worrying about the order of cells within each mutation * @param expectedMutation * @param actualMutation * @return */ boolean isEqualMutation(Mutation expectedMutation, Mutation actualMutation){ List<Cell> expectedCells = new ArrayList<>(); for (List<Cell> cells : expectedMutation.getFamilyCellMap().values()) { expectedCells.addAll(cells); } List<Cell> actualCells = new ArrayList<>(); for (List<Cell> cells : actualMutation.getFamilyCellMap().values()) { actualCells.addAll(cells); } if (expectedCells.size() != actualCells.size()) return false; for(Cell expected : expectedCells) { boolean found = false; for(Cell actual: actualCells){ if (isEqualCell(expected, actual)) { actualCells.remove(actual); found = true; break; } } if (!found) return false; } return true; } boolean isEqualCell(Cell a, Cell b) { return CellUtil.matchingRow(a, b) && CellUtil.matchingFamily(a, b) && CellUtil.matchingQualifier(a, b) && CellUtil.matchingTimestamp(a, b) && CellUtil.matchingType(a, b) && CellUtil.matchingValue(a, b); } }