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;
021import static org.junit.jupiter.api.Assertions.assertNull;
022
023import java.io.IOException;
024import java.util.Arrays;
025import org.apache.hadoop.hbase.Cell;
026import org.apache.hadoop.hbase.KeyValue;
027import org.apache.hadoop.hbase.KeyValueUtil;
028import org.apache.hadoop.hbase.testclassification.FilterTests;
029import org.apache.hadoop.hbase.testclassification.SmallTests;
030import org.apache.hadoop.hbase.util.Bytes;
031import org.apache.hadoop.hbase.util.Pair;
032import org.junit.jupiter.api.Tag;
033import org.junit.jupiter.api.Test;
034
035@Tag(FilterTests.TAG)
036@Tag(SmallTests.TAG)
037public class TestFuzzyRowFilter {
038
039  @Test
040  public void testSatisfiesNoUnsafeForward() {
041
042    assertEquals(FuzzyRowFilter.SatisfiesCode.YES,
043      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, (byte) -128, 1, 0, 1 }, 0, 5,
044        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));
045
046    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
047      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, (byte) -128, 2, 0, 1 }, 0, 5,
048        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));
049
050    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfiesNoUnsafe(false,
051      new byte[] { 1, 2, 1, 3, 3 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));
052
053    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
054      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, 1, 1, 3, 0 }, // row to check
055        0, 5, new byte[] { 1, 2, 0, 3 }, // fuzzy row
056        new byte[] { 0, 0, 1, 0 })); // mask
057
058    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
059      FuzzyRowFilter.satisfiesNoUnsafe(false, new byte[] { 1, 1, 1, 3, 0 }, 0, 5,
060        new byte[] { 1, (byte) 245, 0, 3 }, new byte[] { 0, 0, 1, 0 }));
061
062    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(false,
063      new byte[] { 1, 2, 1, 0, 1 }, 0, 5, new byte[] { 0, 1, 2 }, new byte[] { 1, 0, 0 }));
064  }
065
066  @Test
067  public void testSatisfiesForward() {
068
069    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(false,
070      new byte[] { 1, (byte) -128, 1, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));
071
072    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(false,
073      new byte[] { 1, (byte) -128, 2, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));
074
075    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(false,
076      new byte[] { 1, 2, 1, 3, 3 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
077
078    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
079      FuzzyRowFilter.satisfies(false, new byte[] { 1, 1, 1, 3, 0 }, // row to check
080        new byte[] { 1, 2, 0, 3 }, // fuzzy row
081        new byte[] { -1, -1, 0, -1 })); // mask
082
083    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
084      FuzzyRowFilter.satisfies(false, new byte[] { 1, 1, 1, 3, 0 },
085        new byte[] { 1, (byte) 245, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
086
087    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(false,
088      new byte[] { 1, 2, 1, 0, 1 }, new byte[] { 0, 1, 2 }, new byte[] { 0, -1, -1 }));
089  }
090
091  @Test
092  public void testSatisfiesReverse() {
093    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(true,
094      new byte[] { 1, (byte) -128, 1, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));
095
096    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
097      new byte[] { 1, (byte) -128, 2, 0, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));
098
099    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
100      new byte[] { 2, 3, 1, 1, 1 }, new byte[] { 1, 0, 1 }, new byte[] { -1, 0, -1 }));
101
102    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfies(true,
103      new byte[] { 1, 2, 1, 3, 3 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
104
105    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
106      FuzzyRowFilter.satisfies(true, new byte[] { 1, (byte) 245, 1, 3, 0 },
107        new byte[] { 1, 1, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
108
109    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
110      new byte[] { 1, 3, 1, 3, 0 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
111
112    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
113      new byte[] { 2, 1, 1, 1, 0 }, new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
114
115    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfies(true,
116      new byte[] { 1, 2, 1, 0, 1 }, new byte[] { 0, 1, 2 }, new byte[] { 0, -1, -1 }));
117  }
118
119  @Test
120  public void testSatisfiesNoUnsafeReverse() {
121    assertEquals(FuzzyRowFilter.SatisfiesCode.YES,
122      FuzzyRowFilter.satisfiesNoUnsafe(true, new byte[] { 1, (byte) -128, 1, 0, 1 }, 0, 5,
123        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));
124
125    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
126      FuzzyRowFilter.satisfiesNoUnsafe(true, new byte[] { 1, (byte) -128, 2, 0, 1 }, 0, 5,
127        new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));
128
129    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
130      new byte[] { 2, 3, 1, 1, 1 }, 0, 5, new byte[] { 1, 0, 1 }, new byte[] { 0, 1, 0 }));
131
132    assertEquals(FuzzyRowFilter.SatisfiesCode.YES, FuzzyRowFilter.satisfiesNoUnsafe(true,
133      new byte[] { 1, 2, 1, 3, 3 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));
134
135    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS,
136      FuzzyRowFilter.satisfiesNoUnsafe(true, new byte[] { 1, (byte) 245, 1, 3, 0 }, 0, 5,
137        new byte[] { 1, 1, 0, 3 }, new byte[] { 0, 0, 1, 0 }));
138
139    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
140      new byte[] { 1, 3, 1, 3, 0 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));
141
142    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
143      new byte[] { 2, 1, 1, 1, 0 }, 0, 5, new byte[] { 1, 2, 0, 3 }, new byte[] { 0, 0, 1, 0 }));
144
145    assertEquals(FuzzyRowFilter.SatisfiesCode.NEXT_EXISTS, FuzzyRowFilter.satisfiesNoUnsafe(true,
146      new byte[] { 1, 2, 1, 0, 1 }, 0, 5, new byte[] { 0, 1, 2 }, new byte[] { 1, 0, 0 }));
147  }
148
149  @Test
150  public void testGetNextForFuzzyRuleForward() {
151    assertNext(false, new byte[] { 0, 1, 2 }, // fuzzy row
152      new byte[] { 0, -1, -1 }, // mask
153      new byte[] { 1, 2, 1, 0, 1 }, // current
154      new byte[] { 2, 1, 2 }); // expected next
155
156    assertNext(false, new byte[] { 0, 1, 2 }, // fuzzy row
157      new byte[] { 0, -1, -1 }, // mask
158      new byte[] { 1, 1, 2, 0, 1 }, // current
159      new byte[] { 1, 1, 2, 0, 2 }); // expected next
160
161    assertNext(false, new byte[] { 0, 1, 0, 2, 0 }, // fuzzy row
162      new byte[] { 0, -1, 0, -1, 0 }, // mask
163      new byte[] { 1, 0, 2, 0, 1 }, // current
164      new byte[] { 1, 1, 0, 2 }); // expected next
165
166    assertNext(false, new byte[] { 1, 0, 1 }, // fuzzy row
167      new byte[] { -1, 0, -1 }, // mask
168      new byte[] { 1, (byte) 128, 2, 0, 1 }, // current
169      new byte[] { 1, (byte) 129, 1 }); // expected next
170
171    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
172      new byte[] { 0, -1, 0, -1 }, // mask
173      new byte[] { 5, 1, 0, 1 }, // current
174      new byte[] { 5, 1, 1, 1 }); // expected next
175
176    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
177      new byte[] { 0, -1, 0, -1 }, // mask
178      new byte[] { 5, 1, 0, 1, 1 }, // current
179      new byte[] { 5, 1, 0, 1, 2 }); // expected next
180
181    assertNext(false, new byte[] { 0, 1, 0, 0 }, // fuzzy row
182      new byte[] { 0, -1, 0, 0 }, // mask
183      new byte[] { 5, 1, (byte) 255, 1 }, // current
184      new byte[] { 5, 1, (byte) 255, 2 }); // expected next
185
186    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
187      new byte[] { 0, -1, 0, -1 }, // mask
188      new byte[] { 5, 1, (byte) 255, 1 }, // current
189      new byte[] { 6, 1, 0, 1 }); // expected next
190
191    assertNext(false, new byte[] { 0, 1, 0, 1 }, // fuzzy row
192      new byte[] { 0, -1, 0, -1 }, // mask
193      new byte[] { 5, 1, (byte) 255, 0 }, // current
194      new byte[] { 5, 1, (byte) 255, 1 }); // expected next
195
196    assertNext(false, new byte[] { 5, 1, 1, 0 }, // fuzzy row
197      new byte[] { -1, -1, 0, 0 }, // mask
198      new byte[] { 5, 1, (byte) 255, 1 }, // current
199      new byte[] { 5, 1, (byte) 255, 2 }); // expected next
200
201    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
202      new byte[] { -1, -1, 0, 0 }, // mask
203      new byte[] { 1, 1, 2, 2 }, // current
204      new byte[] { 1, 1, 2, 3 }); // expected next
205
206    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
207      new byte[] { -1, -1, 0, 0 }, // mask
208      new byte[] { 1, 1, 3, 2 }, // current
209      new byte[] { 1, 1, 3, 3 }); // expected next
210
211    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
212      new byte[] { 0, 0, 0, 0 }, // mask
213      new byte[] { 1, 1, 2, 3 }, // current
214      new byte[] { 1, 1, 2, 4 }); // expected next
215
216    assertNext(false, new byte[] { 1, 1, 1, 1 }, // fuzzy row
217      new byte[] { 0, 0, 0, 0 }, // mask
218      new byte[] { 1, 1, 3, 2 }, // current
219      new byte[] { 1, 1, 3, 3 }); // expected next
220
221    assertNext(false, new byte[] { 1, 1, 0, 0 }, // fuzzy row
222      new byte[] { -1, -1, 0, 0 }, // mask
223      new byte[] { 0, 1, 3, 2 }, // current
224      new byte[] { 1, 1 }); // expected next
225
226    // No next for this one
227    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 2, 3, 1, 1, 1 }, // row to
228                                                                                // check
229      new byte[] { 1, 0, 1 }, // fuzzy row
230      new byte[] { -1, 0, -1 })); // mask
231    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 1, (byte) 245, 1, 3, 0 },
232      new byte[] { 1, 1, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
233    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 1, 3, 1, 3, 0 },
234      new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
235    assertNull(FuzzyRowFilter.getNextForFuzzyRule(new byte[] { 2, 1, 1, 1, 0 },
236      new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
237  }
238
239  @Test
240  public void testGetNextForFuzzyRuleReverse() {
241    // In these reverse cases for the next row key the last non-max byte should be increased
242    // to make sure that the proper row is selected next by the scanner.
243    // For example:
244    // fuzzy row: 0,1,2
245    // mask: 0,-1,-1
246    // current: 1,2,1,0,1
247    // next would be: 1,1,2
248    // this has to be increased to 1,1,3 to make sure that the proper row is selected next.
249    assertNext(true, new byte[] { 0, 1, 2 }, // fuzzy row
250      new byte[] { 0, -1, -1 }, // mask
251      new byte[] { 1, 2, 1, 0, 1 }, // current
252      new byte[] { 1, 1, 3 }); // expected next
253
254    // Preserve HBASE-28634 boundary hints. Lowering 1115 to 1114 would skip rows such as 111444.
255    assertNext(true, Bytes.toBytes("111433"), // fuzzy row
256      new byte[] { -1, -1, -1, -1, 0, 0 }, // mask
257      Bytes.toBytes("1115"), // current
258      Bytes.toBytes("1115")); // expected next
259
260    assertNext(true, new byte[] { 0, 1, 0, 2, 0 }, // fuzzy row
261      new byte[] { 0, -1, 0, -1, 0 }, // mask
262      new byte[] { 1, 2, 1, 3, 1 }, // current
263      new byte[] { 1, 1, (byte) 255, 3 }); // expected next
264
265    assertNext(true, new byte[] { 1, 0, 1 }, // fuzzy row
266      new byte[] { -1, 0, -1 }, // mask
267      new byte[] { 1, (byte) 128, 2, 0, 1 }, // current
268      new byte[] { 1, (byte) 128, 2 }); // expected next
269
270    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
271      new byte[] { 0, -1, 0, -1 }, // mask
272      new byte[] { 5, 1, 0, 2, 1 }, // current
273      new byte[] { 5, 1, 0, 2 }); // expected next
274
275    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
276      new byte[] { 0, -1, 0, -1 }, // mask
277      new byte[] { 5, 1, 0, 1 }, // current
278      new byte[] { 4, 1, (byte) 255, 2 }); // expected next
279
280    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
281      new byte[] { 0, -1, 0, -1 }, // mask
282      new byte[] { 5, 1, (byte) 255, 0 }, // current
283      new byte[] { 5, 1, (byte) 254, 2 }); // expected next
284
285    assertNext(true, new byte[] { 1, 1, 0, 0 }, // fuzzy row
286      new byte[] { -1, -1, 0, 0 }, // mask
287      new byte[] { 2, 1, 3, 2 }, // current
288      new byte[] { 1, 2 }); // expected next
289
290    assertNext(true, new byte[] { 1, 0, 1 }, // fuzzy row
291      new byte[] { -1, 0, -1 }, // mask
292      new byte[] { 2, 3, 1, 1, 1 }, // row to check
293      new byte[] { 1, (byte) 255, 2 }); // expected next
294
295    assertNext(true, new byte[] { 1, 1, 0, 3 }, // fuzzy row
296      new byte[] { -1, -1, 0, -1 }, // mask
297      new byte[] { 1, (byte) 245, 1, 3, 0 }, // row to check
298      new byte[] { 1, 1, (byte) 255, 4 }); // expected next
299
300    assertNext(true, new byte[] { 1, 2, 0, 3 }, // fuzzy row
301      new byte[] { -1, -1, 0, -1 }, // mask
302      new byte[] { 1, 3, 1, 3, 0 }, // row to check
303      new byte[] { 1, 2, (byte) 255, 4 }); // expected next
304
305    assertNext(true, new byte[] { 1, 2, 0, 3 }, // fuzzy row
306      new byte[] { -1, -1, 0, -1 }, // mask
307      new byte[] { 2, 1, 1, 1, 0 }, // row to check
308      new byte[] { 1, 2, (byte) 255, 4 }); // expected next
309
310    // No cell before the current one satisfies the fuzzy row -> null.
311    assertNull(FuzzyRowFilter.getNextForFuzzyRule(true, new byte[] { 1, 1, 1, 3, 0 },
312      new byte[] { 1, 2, 0, 3 }, new byte[] { -1, -1, 0, -1 }));
313  }
314
315  @Test
316  public void testGetNextForFuzzyRuleReverseCanReturnCurrentRow() {
317    // HBASE-28634 reverse adjustment can intentionally return the current row as a boundary hint.
318    assertNext(true, new byte[] { 'a', 'a', 'a' }, // fuzzy row
319      new byte[] { -1, 0, -1 }, // mask
320      new byte[] { 'a', 'b', 'b' }, // current
321      new byte[] { 'a', 'b', 'b' }); // expected next
322
323    assertNext(true, new byte[] { 0, 1, 0, 0 }, // fuzzy row
324      new byte[] { 0, -1, 0, 0 }, // mask
325      new byte[] { 5, 1, (byte) 255, 1 }, // current
326      new byte[] { 5, 1, (byte) 255, 1 }); // expected next
327
328    assertNext(true, new byte[] { 1, 0, 1 }, // fuzzy row
329      new byte[] { -1, 0, -1 }, // mask
330      new byte[] { 1, (byte) 128, 2 }, // row to check
331      new byte[] { 1, (byte) 128, 2 }); // expected next
332
333    assertNext(true, new byte[] { 0, 1, 0, 1 }, // fuzzy row
334      new byte[] { 0, -1, 0, -1 }, // mask
335      new byte[] { 5, 1, 0, 2 }, // row to check
336      new byte[] { 5, 1, 0, 2 }); // expected next
337
338    assertNext(true, new byte[] { 5, 1, 1, 0 }, // fuzzy row
339      new byte[] { -1, -1, 0, 0 }, // mask
340      new byte[] { 5, 1, (byte) 0xFF, 1 }, // row to check
341      new byte[] { 5, 1, (byte) 0xFF, 1 }); // expected next
342
343    assertNext(true, new byte[] { 1, 1, 1, 1 }, // fuzzy row
344      new byte[] { -1, -1, 0, 0 }, // mask
345      new byte[] { 1, 1, 2, 2 }, // row to check
346      new byte[] { 1, 1, 2, 2 }); // expected next
347
348    assertNext(true, new byte[] { 1, 1, 1, 1 }, // fuzzy row
349      new byte[] { 0, 0, 0, 0 }, // mask
350      new byte[] { 1, 1, 2, 3 }, // row to check
351      new byte[] { 1, 1, 2, 3 }); // expected next
352  }
353
354  private static void assertNext(boolean reverse, byte[] fuzzyRow, byte[] mask, byte[] current,
355    byte[] expected) {
356    KeyValue kv = KeyValueUtil.createFirstOnRow(current);
357    byte[] nextForFuzzyRule = FuzzyRowFilter.getNextForFuzzyRule(reverse, kv.getRowArray(),
358      kv.getRowOffset(), kv.getRowLength(), fuzzyRow, mask);
359    assertEquals(Bytes.toStringBinary(expected), Bytes.toStringBinary(nextForFuzzyRule));
360  }
361
362  @Test
363  public void testReverseFilterCellSkipsSameRowHint() {
364    // The first non-matching row can seek to abb, but abb would recreate abb as its reverse hint.
365    // The scanner should skip that non-matching row instead of seeking to the same row again.
366    FuzzyRowFilter filter = newReverseFuzzyRowFilter();
367
368    KeyValue abc = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abc"));
369    assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filter.filterCell(abc));
370    Cell hint = filter.getNextCellHint(abc);
371    assertRow("abb", hint);
372
373    KeyValue abb = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abb"));
374    // filterCell handles the same-row hint by skipping the current non-matching row.
375    assertEquals(Filter.ReturnCode.NEXT_ROW, filter.filterCell(abb));
376  }
377
378  @Test
379  public void testReverseFilterListSkipsSameRowFuzzyHint() throws IOException {
380    for (FilterList.Operator operator : Arrays.asList(FilterList.Operator.MUST_PASS_ALL,
381      FilterList.Operator.MUST_PASS_ONE)) {
382      FilterList filterList =
383        new FilterList(operator, newReverseFuzzyRowFilter(), newReverseFuzzyRowFilter());
384
385      KeyValue abc = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abc"));
386      assertEquals(Filter.ReturnCode.SEEK_NEXT_USING_HINT, filterList.filterCell(abc));
387      assertRow("abb", filterList.getNextCellHint(abc));
388
389      KeyValue abb = KeyValueUtil.createFirstOnRow(Bytes.toBytes("abb"));
390      assertEquals(Filter.ReturnCode.NEXT_ROW, filterList.filterCell(abb));
391    }
392  }
393
394  /**
395   * Serializing a filter must not leak the internal mask encoding. On the unsafe path the stored
396   * mask is {-1 (fixed), 0 (non-fixed)}; {@code toByteArray} must emit the public constructor {0,
397   * 1} form so the round-tripped filter behaves identically (otherwise the leaked {-1, 0} is
398   * reparsed as an all-fixed mask). Processing a cell first must not change this.
399   */
400  @Test
401  public void testSerializationAfterFilterCellPreservesBehavior() throws Exception {
402    FuzzyRowFilter original =
403      new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 })));
404    // Process a cell first to prove serialization is unaffected by scanning.
405    original.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 50, 3 }));
406
407    FuzzyRowFilter parsed = FuzzyRowFilter.parseFrom(original.toByteArray());
408    // A row matching only via the wildcard position must still be INCLUDED after the round-trip.
409    assertEquals(Filter.ReturnCode.INCLUDE,
410      parsed.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 99, 3 })));
411  }
412
413  /**
414   * Two filters built from the same rule (distinct array instances) must be equal and hash equally,
415   * i.e. {@code equals}/{@code hashCode} must be content-based, not identity-based, and consistent
416   * with each other and with the serialized ({0, 1}) form. This must also hold after one of them
417   * has processed a cell.
418   */
419  @Test
420  public void testEqualsConsistentAfterFilterCell() {
421    FuzzyRowFilter fresh =
422      new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 })));
423    FuzzyRowFilter scanned =
424      new FuzzyRowFilter(Arrays.asList(new Pair<>(new byte[] { 1, 2, 3 }, new byte[] { 0, 1, 0 })));
425    scanned.filterCell(KeyValueUtil.createFirstOnRow(new byte[] { 1, 50, 3 }));
426
427    assertEquals(fresh, scanned);
428    assertEquals(fresh.hashCode(), scanned.hashCode());
429  }
430
431  private static FuzzyRowFilter newReverseFuzzyRowFilter() {
432    FuzzyRowFilter filter =
433      new FuzzyRowFilter(Arrays.asList(new Pair<>(Bytes.toBytes("aaa"), new byte[] { 0, 1, 0 })));
434    filter.setReversed(true);
435    return filter;
436  }
437
438  private static void assertRow(String expected, Cell cell) {
439    assertEquals(expected,
440      Bytes.toString(cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()));
441  }
442}