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.assertEquals;
021
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029import java.util.Set;
030import org.apache.hadoop.hbase.Cell;
031import org.apache.hadoop.hbase.HBaseTestingUtil;
032import org.apache.hadoop.hbase.KeyValue;
033import org.apache.hadoop.hbase.KeyValueTestUtil;
034import org.apache.hadoop.hbase.TableName;
035import org.apache.hadoop.hbase.client.Durability;
036import org.apache.hadoop.hbase.client.Put;
037import org.apache.hadoop.hbase.client.Result;
038import org.apache.hadoop.hbase.client.ResultScanner;
039import org.apache.hadoop.hbase.client.Scan;
040import org.apache.hadoop.hbase.client.Table;
041import org.apache.hadoop.hbase.filter.FilterList.Operator;
042import org.apache.hadoop.hbase.testclassification.FilterTests;
043import org.apache.hadoop.hbase.testclassification.MediumTests;
044import org.apache.hadoop.hbase.util.Bytes;
045import org.junit.jupiter.api.AfterAll;
046import org.junit.jupiter.api.AfterEach;
047import org.junit.jupiter.api.BeforeAll;
048import org.junit.jupiter.api.BeforeEach;
049import org.junit.jupiter.api.Tag;
050import org.junit.jupiter.api.Test;
051import org.junit.jupiter.api.TestInfo;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055@Tag(FilterTests.TAG)
056@Tag(MediumTests.TAG)
057public class TestColumnRangeFilter {
058
059  private final static HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
060
061  private static final Logger LOG = LoggerFactory.getLogger(TestColumnRangeFilter.class);
062
063  @BeforeAll
064  public static void setUpBeforeClass() throws Exception {
065    TEST_UTIL.startMiniCluster();
066  }
067
068  @AfterAll
069  public static void tearDownAfterClass() throws Exception {
070    TEST_UTIL.shutdownMiniCluster();
071  }
072
073  @BeforeEach
074  public void setUp() throws Exception {
075    // Nothing to do.
076  }
077
078  @AfterEach
079  public void tearDown() throws Exception {
080    // Nothing to do.
081  }
082
083  @Test
084  public void TestColumnRangeFilterClient(TestInfo testInfo) throws Exception {
085    String family = "Family";
086    Table ht = TEST_UTIL.createTable(TableName.valueOf(testInfo.getTestMethod().get().getName()),
087      Bytes.toBytes(family), Integer.MAX_VALUE);
088
089    List<String> rows = generateRandomWords(10, 8);
090    long maxTimestamp = 2;
091    List<String> columns = generateRandomWords(20000, 8);
092
093    List<KeyValue> kvList = new ArrayList<>();
094
095    Map<StringRange, List<KeyValue>> rangeMap = new HashMap<>();
096
097    rangeMap.put(new StringRange(null, true, "b", false), new ArrayList<>());
098    rangeMap.put(new StringRange("p", true, "q", false), new ArrayList<>());
099    rangeMap.put(new StringRange("r", false, "s", true), new ArrayList<>());
100    rangeMap.put(new StringRange("z", false, null, false), new ArrayList<>());
101    String valueString = "ValueString";
102
103    for (String row : rows) {
104      Put p = new Put(Bytes.toBytes(row));
105      p.setDurability(Durability.SKIP_WAL);
106      for (String column : columns) {
107        for (long timestamp = 1; timestamp <= maxTimestamp; timestamp++) {
108          KeyValue kv = KeyValueTestUtil.create(row, family, column, timestamp, valueString);
109          p.add(kv);
110          kvList.add(kv);
111          for (StringRange s : rangeMap.keySet()) {
112            if (s.inRange(column)) {
113              rangeMap.get(s).add(kv);
114            }
115          }
116        }
117      }
118      ht.put(p);
119    }
120
121    TEST_UTIL.flush();
122
123    ColumnRangeFilter filter;
124    Scan scan = new Scan();
125    scan.readAllVersions();
126    for (StringRange s : rangeMap.keySet()) {
127      filter = new ColumnRangeFilter(s.getStart() == null ? null : Bytes.toBytes(s.getStart()),
128        s.isStartInclusive(), s.getEnd() == null ? null : Bytes.toBytes(s.getEnd()),
129        s.isEndInclusive());
130      assertEquals(rangeMap.get(s).size(), cellsCount(ht, filter));
131    }
132    ht.close();
133  }
134
135  @Test
136  public void TestColumnRangeFilterWithColumnPaginationFilter() throws Exception {
137    String family = "Family";
138    String table = "TestColumnRangeFilterWithColumnPaginationFilter";
139    try (Table ht =
140      TEST_UTIL.createTable(TableName.valueOf(table), Bytes.toBytes(family), Integer.MAX_VALUE)) {
141      // one row.
142      String row = "row";
143      // One version
144      long timestamp = 100;
145      // 10 columns
146      int[] columns = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
147      String valueString = "ValueString";
148
149      Put p = new Put(Bytes.toBytes(row));
150      p.setDurability(Durability.SKIP_WAL);
151      for (int column : columns) {
152        KeyValue kv =
153          KeyValueTestUtil.create(row, family, Integer.toString(column), timestamp, valueString);
154        p.add(kv);
155      }
156      ht.put(p);
157
158      TEST_UTIL.flush();
159
160      // Column range from 1 to 9.
161      StringRange stringRange = new StringRange("1", true, "9", false);
162      ColumnRangeFilter filter1 =
163        new ColumnRangeFilter(Bytes.toBytes(stringRange.getStart()), stringRange.isStartInclusive(),
164          Bytes.toBytes(stringRange.getEnd()), stringRange.isEndInclusive());
165
166      ColumnPaginationFilter filter2 = new ColumnPaginationFilter(5, 0);
167      ColumnPaginationFilter filter3 = new ColumnPaginationFilter(5, 1);
168      ColumnPaginationFilter filter4 = new ColumnPaginationFilter(5, 2);
169      ColumnPaginationFilter filter5 = new ColumnPaginationFilter(5, 6);
170      ColumnPaginationFilter filter6 = new ColumnPaginationFilter(5, 9);
171      assertEquals(5, cellsCount(ht, new FilterList(Operator.MUST_PASS_ALL, filter1, filter2)));
172      assertEquals(5, cellsCount(ht, new FilterList(Operator.MUST_PASS_ALL, filter1, filter3)));
173      assertEquals(5, cellsCount(ht, new FilterList(Operator.MUST_PASS_ALL, filter1, filter4)));
174      assertEquals(2, cellsCount(ht, new FilterList(Operator.MUST_PASS_ALL, filter1, filter5)));
175      assertEquals(0, cellsCount(ht, new FilterList(Operator.MUST_PASS_ALL, filter1, filter6)));
176    }
177  }
178
179  private int cellsCount(Table table, Filter filter) throws IOException {
180    Scan scan = new Scan().setFilter(filter).readAllVersions();
181    try (ResultScanner scanner = table.getScanner(scan)) {
182      List<Cell> results = new ArrayList<>();
183      Result result;
184      while ((result = scanner.next()) != null) {
185        result.listCells().forEach(results::add);
186      }
187      return results.size();
188    }
189  }
190
191  List<String> generateRandomWords(int numberOfWords, int maxLengthOfWords) {
192    Set<String> wordSet = new HashSet<>();
193    for (int i = 0; i < numberOfWords; i++) {
194      int lengthOfWords = (int) (Math.random() * maxLengthOfWords) + 1;
195      char[] wordChar = new char[lengthOfWords];
196      for (int j = 0; j < wordChar.length; j++) {
197        wordChar[j] = (char) (Math.random() * 26 + 97);
198      }
199      String word = new String(wordChar);
200      wordSet.add(word);
201    }
202    List<String> wordList = new ArrayList<>(wordSet);
203    return wordList;
204  }
205
206}
207
208class StringRange {
209  private String start = null;
210  private String end = null;
211  private boolean startInclusive = true;
212  private boolean endInclusive = false;
213
214  public StringRange(String start, boolean startInclusive, String end, boolean endInclusive) {
215    this.start = start;
216    this.startInclusive = startInclusive;
217    this.end = end;
218    this.endInclusive = endInclusive;
219  }
220
221  public String getStart() {
222    return this.start;
223  }
224
225  public String getEnd() {
226    return this.end;
227  }
228
229  public boolean isStartInclusive() {
230    return this.startInclusive;
231  }
232
233  public boolean isEndInclusive() {
234    return this.endInclusive;
235  }
236
237  @Override
238  public int hashCode() {
239    int hashCode = 0;
240    if (this.start != null) {
241      hashCode ^= this.start.hashCode();
242    }
243
244    if (this.end != null) {
245      hashCode ^= this.end.hashCode();
246    }
247    return hashCode;
248  }
249
250  @Override
251  public boolean equals(Object obj) {
252    if (this == obj) {
253      return true;
254    }
255    if (obj == null) {
256      return false;
257    }
258    if (!(obj instanceof StringRange)) {
259      return false;
260    }
261    StringRange oth = (StringRange) obj;
262    return this.startInclusive == oth.startInclusive && this.endInclusive == oth.endInclusive
263      && Objects.equals(this.start, oth.start) && Objects.equals(this.end, oth.end);
264  }
265
266  @Override
267  public String toString() {
268    String result = (this.startInclusive ? "[" : "(") + (this.start == null ? null : this.start)
269      + ", " + (this.end == null ? null : this.end) + (this.endInclusive ? "]" : ")");
270    return result;
271  }
272
273  public boolean inRange(String value) {
274    boolean afterStart = true;
275    if (this.start != null) {
276      int startCmp = value.compareTo(this.start);
277      afterStart = this.startInclusive ? startCmp >= 0 : startCmp > 0;
278    }
279
280    boolean beforeEnd = true;
281    if (this.end != null) {
282      int endCmp = value.compareTo(this.end);
283      beforeEnd = this.endInclusive ? endCmp <= 0 : endCmp < 0;
284    }
285
286    return afterStart && beforeEnd;
287  }
288}