001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.filter;
019
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.List;
024import org.apache.commons.codec.binary.Hex;
025import org.apache.hadoop.hbase.HBaseClassTestRule;
026import org.apache.hadoop.hbase.TableName;
027import org.apache.hadoop.hbase.client.Put;
028import org.apache.hadoop.hbase.client.Result;
029import org.apache.hadoop.hbase.client.ResultScanner;
030import org.apache.hadoop.hbase.client.Scan;
031import org.apache.hadoop.hbase.client.Table;
032import org.apache.hadoop.hbase.testclassification.FilterTests;
033import org.apache.hadoop.hbase.testclassification.MediumTests;
034import org.apache.hadoop.hbase.util.Bytes;
035import org.junit.Assert;
036import org.junit.ClassRule;
037import org.junit.Rule;
038import org.junit.Test;
039import org.junit.experimental.categories.Category;
040import org.junit.rules.TestName;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044/**
045 * Test if Scan.setStartStopRowForPrefixScan works as intended.
046 */
047@Category({ FilterTests.class, MediumTests.class })
048public class TestScanRowPrefix extends FilterTestingCluster {
049
050  @ClassRule
051  public static final HBaseClassTestRule CLASS_RULE =
052    HBaseClassTestRule.forClass(TestScanRowPrefix.class);
053
054  private static final Logger LOG = LoggerFactory.getLogger(TestScanRowPrefix.class);
055
056  @Rule
057  public TestName name = new TestName();
058
059  @Test
060  public void testPrefixScanning() throws IOException {
061    final TableName tableName = TableName.valueOf(name.getMethodName());
062    createTable(tableName, "F");
063    Table table = openTable(tableName);
064
065    /**
066     * Note that about half of these tests were relevant for a different implementation approach of
067     * setStartStopRowForPrefixScan. These test cases have been retained to ensure that also the
068     * edge cases found there are still covered.
069     */
070
071    final byte[][] rowIds = { { (byte) 0x11 }, // 0
072      { (byte) 0x12 }, // 1
073      { (byte) 0x12, (byte) 0x23, (byte) 0xFF, (byte) 0xFE }, // 2
074      { (byte) 0x12, (byte) 0x23, (byte) 0xFF, (byte) 0xFF }, // 3
075      { (byte) 0x12, (byte) 0x23, (byte) 0xFF, (byte) 0xFF, (byte) 0x00 }, // 4
076      { (byte) 0x12, (byte) 0x23, (byte) 0xFF, (byte) 0xFF, (byte) 0x01 }, // 5
077      { (byte) 0x12, (byte) 0x24 }, // 6
078      { (byte) 0x12, (byte) 0x24, (byte) 0x00 }, // 7
079      { (byte) 0x12, (byte) 0x24, (byte) 0x00, (byte) 0x00 }, // 8
080      { (byte) 0x12, (byte) 0x25 }, // 9
081      { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }, // 10
082    };
083    for (byte[] rowId : rowIds) {
084      Put p = new Put(rowId);
085      // Use the rowId as the column qualifier
086      p.addColumn(Bytes.toBytes("F"), rowId, Bytes.toBytes("Dummy value"));
087      table.put(p);
088    }
089
090    byte[] prefix0 = {};
091    List<byte[]> expected0 = new ArrayList<>(16);
092    expected0.addAll(Arrays.asList(rowIds)); // Expect all rows
093
094    byte[] prefix1 = { (byte) 0x12, (byte) 0x23 };
095    List<byte[]> expected1 = new ArrayList<>(16);
096    expected1.add(rowIds[2]);
097    expected1.add(rowIds[3]);
098    expected1.add(rowIds[4]);
099    expected1.add(rowIds[5]);
100
101    byte[] prefix2 = { (byte) 0x12, (byte) 0x23, (byte) 0xFF, (byte) 0xFF };
102    List<byte[]> expected2 = new ArrayList<>();
103    expected2.add(rowIds[3]);
104    expected2.add(rowIds[4]);
105    expected2.add(rowIds[5]);
106
107    byte[] prefix3 = { (byte) 0x12, (byte) 0x24 };
108    List<byte[]> expected3 = new ArrayList<>();
109    expected3.add(rowIds[6]);
110    expected3.add(rowIds[7]);
111    expected3.add(rowIds[8]);
112
113    byte[] prefix4 = { (byte) 0xFF, (byte) 0xFF };
114    List<byte[]> expected4 = new ArrayList<>();
115    expected4.add(rowIds[10]);
116
117    // ========
118    // PREFIX 0
119    Scan scan = new Scan();
120    scan.setStartStopRowForPrefixScan(prefix0);
121    verifyScanResult(table, scan, expected0, "Scan empty prefix failed");
122
123    // ========
124    // PREFIX 1
125    scan = new Scan();
126    scan.setStartStopRowForPrefixScan(prefix1);
127    verifyScanResult(table, scan, expected1, "Scan normal prefix failed");
128
129    scan.setStartStopRowForPrefixScan(null);
130    verifyScanResult(table, scan, expected0, "Scan after prefix reset failed");
131
132    scan = new Scan();
133    scan.setFilter(new ColumnPrefixFilter(prefix1));
134    verifyScanResult(table, scan, expected1, "Double check on column prefix failed");
135
136    // ========
137    // PREFIX 2
138    scan = new Scan();
139    scan.setStartStopRowForPrefixScan(prefix2);
140    verifyScanResult(table, scan, expected2, "Scan edge 0xFF prefix failed");
141
142    scan.setStartStopRowForPrefixScan(null);
143    verifyScanResult(table, scan, expected0, "Scan after prefix reset failed");
144
145    scan = new Scan();
146    scan.setFilter(new ColumnPrefixFilter(prefix2));
147    verifyScanResult(table, scan, expected2, "Double check on column prefix failed");
148
149    // ========
150    // PREFIX 3
151    scan = new Scan();
152    scan.setStartStopRowForPrefixScan(prefix3);
153    verifyScanResult(table, scan, expected3, "Scan normal with 0x00 ends failed");
154
155    scan.setStartStopRowForPrefixScan(null);
156    verifyScanResult(table, scan, expected0, "Scan after prefix reset failed");
157
158    scan = new Scan();
159    scan.setFilter(new ColumnPrefixFilter(prefix3));
160    verifyScanResult(table, scan, expected3, "Double check on column prefix failed");
161
162    // ========
163    // PREFIX 4
164    scan = new Scan();
165    scan.setStartStopRowForPrefixScan(prefix4);
166    verifyScanResult(table, scan, expected4, "Scan end prefix failed");
167
168    scan.setStartStopRowForPrefixScan(null);
169    verifyScanResult(table, scan, expected0, "Scan after prefix reset failed");
170
171    scan = new Scan();
172    scan.setFilter(new ColumnPrefixFilter(prefix4));
173    verifyScanResult(table, scan, expected4, "Double check on column prefix failed");
174
175    // ========
176    // COMBINED
177    // Prefix + Filter
178    scan = new Scan();
179    scan.setStartStopRowForPrefixScan(prefix1);
180    verifyScanResult(table, scan, expected1, "Prefix filter failed");
181
182    scan.setFilter(new ColumnPrefixFilter(prefix2));
183    verifyScanResult(table, scan, expected2, "Combined Prefix + Filter failed");
184
185    scan.setStartStopRowForPrefixScan(null);
186    verifyScanResult(table, scan, expected2, "Combined Prefix + Filter; removing Prefix failed");
187
188    scan.setFilter(null);
189    verifyScanResult(table, scan, expected0, "Scan after Filter reset failed");
190
191    // ========
192    // Reversed: Filter + Prefix
193    scan = new Scan();
194    scan.setFilter(new ColumnPrefixFilter(prefix2));
195    verifyScanResult(table, scan, expected2, "Test filter failed");
196
197    scan.setStartStopRowForPrefixScan(prefix1);
198    verifyScanResult(table, scan, expected2, "Combined Filter + Prefix failed");
199
200    scan.setFilter(null);
201    verifyScanResult(table, scan, expected1, "Combined Filter + Prefix ; removing Filter failed");
202
203    scan.setStartStopRowForPrefixScan(null);
204    verifyScanResult(table, scan, expected0, "Scan after prefix reset failed");
205  }
206
207  private void verifyScanResult(Table table, Scan scan, List<byte[]> expectedKeys, String message) {
208    List<byte[]> actualKeys = new ArrayList<>();
209    try {
210      ResultScanner scanner = table.getScanner(scan);
211      for (Result result : scanner) {
212        actualKeys.add(result.getRow());
213      }
214
215      String fullMessage = message;
216      if (LOG.isDebugEnabled()) {
217        fullMessage = message + "\n"
218          + tableOfTwoListsOfByteArrays("Expected", expectedKeys, "Actual  ", actualKeys);
219      }
220
221      Assert.assertArrayEquals(fullMessage, expectedKeys.toArray(), actualKeys.toArray());
222    } catch (IOException e) {
223      e.printStackTrace();
224      Assert.fail();
225    }
226  }
227
228  private String printMultiple(char letter, int count) {
229    StringBuilder sb = new StringBuilder(count);
230    for (int i = 0; i < count; i++) {
231      sb.append(letter);
232    }
233    return sb.toString();
234  }
235
236  private String tableOfTwoListsOfByteArrays(String label1, List<byte[]> listOfBytes1,
237    String label2, List<byte[]> listOfBytes2) {
238    int margin1 = calculateWidth(label1, listOfBytes1);
239    int margin2 = calculateWidth(label2, listOfBytes2);
240
241    StringBuilder sb = new StringBuilder(512);
242    String separator = '+' + printMultiple('-', margin1 + margin2 + 5) + '+' + '\n';
243    sb.append(separator);
244    sb.append(printLine(label1, margin1, label2, margin2)).append('\n');
245    sb.append(separator);
246    int maxLength = Math.max(listOfBytes1.size(), listOfBytes2.size());
247    for (int offset = 0; offset < maxLength; offset++) {
248      String value1 = getStringFromList(listOfBytes1, offset);
249      String value2 = getStringFromList(listOfBytes2, offset);
250      sb.append(printLine(value1, margin1, value2, margin2)).append('\n');
251    }
252    sb.append(separator).append('\n');
253    return sb.toString();
254  }
255
256  private String printLine(String leftValue, int leftWidth1, String rightValue, int rightWidth) {
257    return "| " + leftValue + printMultiple(' ', leftWidth1 - leftValue.length()) + " | "
258      + rightValue + printMultiple(' ', rightWidth - rightValue.length()) + " |";
259  }
260
261  private int calculateWidth(String label1, List<byte[]> listOfBytes1) {
262    int longestList1 = label1.length();
263    for (byte[] value : listOfBytes1) {
264      longestList1 = Math.max(value.length * 2, longestList1);
265    }
266    return longestList1 + 5;
267  }
268
269  private String getStringFromList(List<byte[]> listOfBytes, int offset) {
270    String value1;
271    if (listOfBytes.size() > offset) {
272      value1 = Hex.encodeHexString(listOfBytes.get(offset));
273    } else {
274      value1 = "<missing>";
275    }
276    return value1;
277  }
278
279}