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