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.regionserver.querymatcher;
019
020import static org.junit.Assert.assertArrayEquals;
021import static org.junit.Assert.assertEquals;
022import static org.junit.Assert.assertFalse;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.List;
027import org.apache.hadoop.hbase.Cell;
028import org.apache.hadoop.hbase.ExtendedCell;
029import org.apache.hadoop.hbase.HBaseClassTestRule;
030import org.apache.hadoop.hbase.HConstants;
031import org.apache.hadoop.hbase.KeepDeletedCells;
032import org.apache.hadoop.hbase.KeyValue;
033import org.apache.hadoop.hbase.KeyValue.Type;
034import org.apache.hadoop.hbase.PrivateCellUtil;
035import org.apache.hadoop.hbase.client.Scan;
036import org.apache.hadoop.hbase.filter.FilterBase;
037import org.apache.hadoop.hbase.regionserver.ScanInfo;
038import org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode;
039import org.apache.hadoop.hbase.testclassification.RegionServerTests;
040import org.apache.hadoop.hbase.testclassification.SmallTests;
041import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
042import org.junit.ClassRule;
043import org.junit.Test;
044import org.junit.experimental.categories.Category;
045import org.slf4j.Logger;
046import org.slf4j.LoggerFactory;
047
048@Category({ RegionServerTests.class, SmallTests.class })
049public class TestUserScanQueryMatcher extends AbstractTestScanQueryMatcher {
050
051  @ClassRule
052  public static final HBaseClassTestRule CLASS_RULE =
053    HBaseClassTestRule.forClass(TestUserScanQueryMatcher.class);
054
055  private static final Logger LOG = LoggerFactory.getLogger(TestUserScanQueryMatcher.class);
056
057  /**
058   * This is a cryptic test. It is checking that we don't include a fake cell. See HBASE-16074 for
059   * background.
060   */
061  @Test
062  public void testNeverIncludeFakeCell() throws IOException {
063    long now = EnvironmentEdgeManager.currentTime();
064    // Do with fam2 which has a col2 qualifier.
065    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan,
066      new ScanInfo(this.conf, fam2, 10, 1, ttl, KeepDeletedCells.FALSE,
067        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
068      get.getFamilyMap().get(fam2), now - ttl, now, null);
069    ExtendedCell kv = new KeyValue(row1, fam2, col2, 1, data);
070    ExtendedCell cell = PrivateCellUtil.createLastOnRowCol(kv);
071    qm.setToNewRow(kv);
072    MatchCode code = qm.match(cell);
073    assertFalse(code.compareTo(MatchCode.SEEK_NEXT_COL) != 0);
074  }
075
076  @Test
077  public void testMatchExplicitColumns() throws IOException {
078    // Moving up from the Tracker by using Gets and List<KeyValue> instead
079    // of just byte []
080
081    // Expected result
082    List<MatchCode> expected = new ArrayList<>(6);
083    expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL);
084    expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL);
085    expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL);
086    expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL);
087    expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW);
088    expected.add(ScanQueryMatcher.MatchCode.DONE);
089
090    long now = EnvironmentEdgeManager.currentTime();
091    // 2,4,5
092    UserScanQueryMatcher qm = UserScanQueryMatcher.create(
093      scan, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE,
094        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
095      get.getFamilyMap().get(fam2), now - ttl, now, null);
096
097    List<KeyValue> memstore = new ArrayList<>(6);
098    memstore.add(new KeyValue(row1, fam2, col1, 1, data));
099    memstore.add(new KeyValue(row1, fam2, col2, 1, data));
100    memstore.add(new KeyValue(row1, fam2, col3, 1, data));
101    memstore.add(new KeyValue(row1, fam2, col4, 1, data));
102    memstore.add(new KeyValue(row1, fam2, col5, 1, data));
103
104    memstore.add(new KeyValue(row2, fam1, col1, data));
105
106    List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size());
107    KeyValue k = memstore.get(0);
108    qm.setToNewRow(k);
109
110    for (KeyValue kv : memstore) {
111      actual.add(qm.match(kv));
112    }
113
114    assertEquals(expected.size(), actual.size());
115    for (int i = 0; i < expected.size(); i++) {
116      LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i));
117      assertEquals(expected.get(i), actual.get(i));
118    }
119  }
120
121  @Test
122  public void testMatch_Wildcard() throws IOException {
123    // Moving up from the Tracker by using Gets and List<KeyValue> instead
124    // of just byte []
125
126    // Expected result
127    List<MatchCode> expected = new ArrayList<>(6);
128    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
129    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
130    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
131    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
132    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
133    expected.add(ScanQueryMatcher.MatchCode.DONE);
134
135    long now = EnvironmentEdgeManager.currentTime();
136    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam2, 0, 1,
137      ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null,
138      now - ttl, now, null);
139
140    List<KeyValue> memstore = new ArrayList<>(6);
141    memstore.add(new KeyValue(row1, fam2, col1, 1, data));
142    memstore.add(new KeyValue(row1, fam2, col2, 1, data));
143    memstore.add(new KeyValue(row1, fam2, col3, 1, data));
144    memstore.add(new KeyValue(row1, fam2, col4, 1, data));
145    memstore.add(new KeyValue(row1, fam2, col5, 1, data));
146    memstore.add(new KeyValue(row2, fam1, col1, 1, data));
147
148    List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size());
149
150    KeyValue k = memstore.get(0);
151    qm.setToNewRow(k);
152
153    for (KeyValue kv : memstore) {
154      actual.add(qm.match(kv));
155    }
156
157    assertEquals(expected.size(), actual.size());
158    for (int i = 0; i < expected.size(); i++) {
159      LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i));
160      assertEquals(expected.get(i), actual.get(i));
161    }
162  }
163
164  /**
165   * Verify that {@link ScanQueryMatcher} only skips expired KeyValue instances and does not exit
166   * early from the row (skipping later non-expired KeyValues). This version mimics a Get with
167   * explicitly specified column qualifiers.
168   */
169  @Test
170  public void testMatch_ExpiredExplicit() throws IOException {
171
172    long testTTL = 1000;
173    MatchCode[] expected = new MatchCode[] { ScanQueryMatcher.MatchCode.SEEK_NEXT_COL,
174      ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL,
175      ScanQueryMatcher.MatchCode.SEEK_NEXT_COL,
176      ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL,
177      ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW, ScanQueryMatcher.MatchCode.DONE };
178
179    long now = EnvironmentEdgeManager.currentTime();
180    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan,
181      new ScanInfo(this.conf, fam2, 0, 1, testTTL, KeepDeletedCells.FALSE,
182        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
183      get.getFamilyMap().get(fam2), now - testTTL, now, null);
184
185    KeyValue[] kvs = new KeyValue[] { new KeyValue(row1, fam2, col1, now - 100, data),
186      new KeyValue(row1, fam2, col2, now - 50, data),
187      new KeyValue(row1, fam2, col3, now - 5000, data),
188      new KeyValue(row1, fam2, col4, now - 500, data),
189      new KeyValue(row1, fam2, col5, now - 10000, data),
190      new KeyValue(row2, fam1, col1, now - 10, data) };
191
192    KeyValue k = kvs[0];
193    qm.setToNewRow(k);
194
195    List<MatchCode> actual = new ArrayList<>(kvs.length);
196    for (KeyValue kv : kvs) {
197      actual.add(qm.match(kv));
198    }
199
200    assertEquals(expected.length, actual.size());
201    for (int i = 0; i < expected.length; i++) {
202      LOG.debug("expected " + expected[i] + ", actual " + actual.get(i));
203      assertEquals(expected[i], actual.get(i));
204    }
205  }
206
207  /**
208   * Verify that {@link ScanQueryMatcher} only skips expired KeyValue instances and does not exit
209   * early from the row (skipping later non-expired KeyValues). This version mimics a Get with
210   * wildcard-inferred column qualifiers.
211   */
212  @Test
213  public void testMatch_ExpiredWildcard() throws IOException {
214
215    long testTTL = 1000;
216    MatchCode[] expected =
217      new MatchCode[] { ScanQueryMatcher.MatchCode.INCLUDE, ScanQueryMatcher.MatchCode.INCLUDE,
218        ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, ScanQueryMatcher.MatchCode.INCLUDE,
219        ScanQueryMatcher.MatchCode.SEEK_NEXT_COL, ScanQueryMatcher.MatchCode.DONE };
220
221    long now = EnvironmentEdgeManager.currentTime();
222    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam2, 0, 1,
223      testTTL, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null,
224      now - testTTL, now, null);
225
226    KeyValue[] kvs = new KeyValue[] { new KeyValue(row1, fam2, col1, now - 100, data),
227      new KeyValue(row1, fam2, col2, now - 50, data),
228      new KeyValue(row1, fam2, col3, now - 5000, data),
229      new KeyValue(row1, fam2, col4, now - 500, data),
230      new KeyValue(row1, fam2, col5, now - 10000, data),
231      new KeyValue(row2, fam1, col1, now - 10, data) };
232    KeyValue k = kvs[0];
233    qm.setToNewRow(k);
234
235    List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(kvs.length);
236    for (KeyValue kv : kvs) {
237      actual.add(qm.match(kv));
238    }
239
240    assertEquals(expected.length, actual.size());
241    for (int i = 0; i < expected.length; i++) {
242      LOG.debug("expected " + expected[i] + ", actual " + actual.get(i));
243      assertEquals(expected[i], actual.get(i));
244    }
245  }
246
247  private static class AlwaysIncludeAndSeekNextRowFilter extends FilterBase {
248    @Override
249    public ReturnCode filterCell(final Cell c) {
250      return ReturnCode.INCLUDE_AND_SEEK_NEXT_ROW;
251    }
252  }
253
254  @Test
255  public void testMatchWhenFilterReturnsIncludeAndSeekNextRow() throws IOException {
256    List<MatchCode> expected = new ArrayList<>();
257    expected.add(ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW);
258    expected.add(ScanQueryMatcher.MatchCode.DONE);
259
260    Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeAndSeekNextRowFilter());
261
262    long now = EnvironmentEdgeManager.currentTime();
263
264    // scan with column 2,4,5
265    UserScanQueryMatcher qm = UserScanQueryMatcher.create(
266      scanWithFilter, new ScanInfo(this.conf, fam2, 0, 1, ttl, KeepDeletedCells.FALSE,
267        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
268      get.getFamilyMap().get(fam2), now - ttl, now, null);
269
270    List<KeyValue> memstore = new ArrayList<>();
271    // ColumnTracker will return INCLUDE_AND_SEEK_NEXT_COL , and filter will return
272    // INCLUDE_AND_SEEK_NEXT_ROW, so final match code will be INCLUDE_AND_SEEK_NEXT_ROW.
273    memstore.add(new KeyValue(row1, fam2, col2, 1, data));
274    memstore.add(new KeyValue(row2, fam1, col1, data));
275
276    List<ScanQueryMatcher.MatchCode> actual = new ArrayList<>(memstore.size());
277    KeyValue k = memstore.get(0);
278    qm.setToNewRow(k);
279
280    for (KeyValue kv : memstore) {
281      actual.add(qm.match(kv));
282    }
283
284    assertEquals(expected.size(), actual.size());
285    for (int i = 0; i < expected.size(); i++) {
286      LOG.debug("expected " + expected.get(i) + ", actual " + actual.get(i));
287      assertEquals(expected.get(i), actual.get(i));
288    }
289  }
290
291  private static class AlwaysIncludeFilter extends FilterBase {
292    @Override
293    public ReturnCode filterCell(final Cell c) {
294      return ReturnCode.INCLUDE;
295    }
296  }
297
298  /**
299   * Here is the unit test for UserScanQueryMatcher#mergeFilterResponse, when the number of cells
300   * exceed the versions requested in scan, we should return SEEK_NEXT_COL, but if current match
301   * code is INCLUDE_AND_SEEK_NEXT_ROW, we can optimize to choose the max step between SEEK_NEXT_COL
302   * and INCLUDE_AND_SEEK_NEXT_ROW, which is SEEK_NEXT_ROW. <br/>
303   */
304  @Test
305  public void testMergeFilterResponseCase1() throws IOException {
306    List<MatchCode> expected = new ArrayList<>();
307    expected.add(MatchCode.INCLUDE);
308    expected.add(MatchCode.INCLUDE);
309    expected.add(MatchCode.SEEK_NEXT_ROW);
310
311    Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(2);
312
313    long now = EnvironmentEdgeManager.currentTime();
314    // scan with column 2,4,5, the family with maxVersion = 3
315    UserScanQueryMatcher qm = UserScanQueryMatcher.create(
316      scanWithFilter, new ScanInfo(this.conf, fam2, 0, 3, ttl, KeepDeletedCells.FALSE,
317        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
318      get.getFamilyMap().get(fam2), now - ttl, now, null);
319
320    List<KeyValue> memstore = new ArrayList<>();
321    memstore.add(new KeyValue(row1, fam1, col5, 1, data)); // match code will be INCLUDE
322    memstore.add(new KeyValue(row1, fam1, col5, 2, data)); // match code will be INCLUDE
323
324    // match code will be SEEK_NEXT_ROW , which is max(INCLUDE_AND_SEEK_NEXT_ROW, SEEK_NEXT_COL).
325    memstore.add(new KeyValue(row1, fam1, col5, 3, data));
326
327    KeyValue k = memstore.get(0);
328    qm.setToNewRow(k);
329
330    for (int i = 0; i < memstore.size(); i++) {
331      assertEquals(expected.get(i), qm.match(memstore.get(i)));
332    }
333
334    scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(1);
335    qm = UserScanQueryMatcher.create(
336      scanWithFilter, new ScanInfo(this.conf, fam2, 0, 2, ttl, KeepDeletedCells.FALSE,
337        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
338      get.getFamilyMap().get(fam2), now - ttl, now, null);
339
340    List<KeyValue> memstore2 = new ArrayList<>();
341    memstore2.add(new KeyValue(row2, fam1, col2, 1, data)); // match code will be INCLUDE
342    // match code will be SEEK_NEXT_COL, which is max(INCLUDE_AND_SEEK_NEXT_COL, SEEK_NEXT_COL).
343    memstore2.add(new KeyValue(row2, fam1, col2, 2, data));
344
345    k = memstore2.get(0);
346    qm.setToNewRow(k);
347
348    assertEquals(MatchCode.INCLUDE, qm.match(memstore2.get(0)));
349    assertEquals(MatchCode.SEEK_NEXT_COL, qm.match(memstore2.get(1)));
350  }
351
352  /**
353   * Here is the unit test for UserScanQueryMatcher#mergeFilterResponse: the match code may be
354   * changed to SEEK_NEXT_COL or INCLUDE_AND_SEEK_NEXT_COL after merging with filterResponse, even
355   * if the passed match code is neither SEEK_NEXT_COL nor INCLUDE_AND_SEEK_NEXT_COL. In that case,
356   * we need to make sure that the ColumnTracker has been switched to the next column. <br/>
357   * An effective test way is: we only need to check the cell from getKeyForNextColumn(). because
358   * that as long as the UserScanQueryMatcher returns SEEK_NEXT_COL or INCLUDE_AND_SEEK_NEXT_COL,
359   * UserScanQueryMatcher#getKeyForNextColumn should return an cell whose column is larger than the
360   * current cell's.
361   */
362  @Test
363  public void testMergeFilterResponseCase2() throws Exception {
364    List<MatchCode> expected = new ArrayList<>();
365    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
366    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
367    expected.add(ScanQueryMatcher.MatchCode.INCLUDE);
368    expected.add(ScanQueryMatcher.MatchCode.SEEK_NEXT_COL);
369
370    Scan scanWithFilter = new Scan(scan).setFilter(new AlwaysIncludeFilter()).readVersions(3);
371
372    long now = EnvironmentEdgeManager.currentTime();
373
374    // scan with column 2,4,5, the family with maxVersion = 5
375    UserScanQueryMatcher qm = UserScanQueryMatcher.create(
376      scanWithFilter, new ScanInfo(this.conf, fam2, 0, 5, ttl, KeepDeletedCells.FALSE,
377        HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false),
378      get.getFamilyMap().get(fam2), now - ttl, now, null);
379
380    List<KeyValue> memstore = new ArrayList<>();
381
382    memstore.add(new KeyValue(row1, fam1, col2, 1, data)); // match code will be INCLUDE
383    memstore.add(new KeyValue(row1, fam1, col2, 2, data)); // match code will be INCLUDE
384    memstore.add(new KeyValue(row1, fam1, col2, 3, data)); // match code will be INCLUDE
385    memstore.add(new KeyValue(row1, fam1, col2, 4, data)); // match code will be SEEK_NEXT_COL
386
387    KeyValue k = memstore.get(0);
388    qm.setToNewRow(k);
389
390    for (int i = 0; i < memstore.size(); i++) {
391      assertEquals(expected.get(i), qm.match(memstore.get(i)));
392    }
393
394    // For last cell, the query matcher will return SEEK_NEXT_COL, and the
395    // ColumnTracker will skip to the next column, which is col4.
396    ExtendedCell lastCell = memstore.get(memstore.size() - 1);
397    Cell nextCell = qm.getKeyForNextColumn(lastCell);
398    assertArrayEquals(nextCell.getQualifierArray(), col4);
399  }
400
401  /**
402   * After enough consecutive range delete markers, the matcher should switch from SKIP to
403   * SEEK_NEXT_COL. Point deletes and KEEP_DELETED_CELLS always SKIP.
404   */
405  @Test
406  public void testSeekOnRangeDelete() throws IOException {
407    int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD;
408
409    // DeleteColumn: first N-1 SKIP, N-th triggers SEEK_NEXT_COL
410    assertSeekAfterThreshold(KeepDeletedCells.FALSE, Type.DeleteColumn, n);
411
412    // DeleteFamily: same threshold behavior
413    assertSeekAfterThreshold(KeepDeletedCells.FALSE, Type.DeleteFamily, n);
414
415    // Delete (version): always SKIP (point delete, not range)
416    assertAllSkip(KeepDeletedCells.FALSE, Type.Delete, n + 1);
417
418    // KEEP_DELETED_CELLS=TRUE: always SKIP
419    assertAllSkip(KeepDeletedCells.TRUE, Type.DeleteColumn, n + 1);
420  }
421
422  /**
423   * DeleteColumn with empty qualifier must not cause seeking past a subsequent DeleteFamily.
424   * DeleteFamily masks all columns, so it must be tracked by the delete tracker.
425   */
426  @Test
427  public void testDeleteColumnEmptyQualifierDoesNotSkipDeleteFamily() throws IOException {
428    long now = EnvironmentEdgeManager.currentTime();
429    byte[] e = HConstants.EMPTY_BYTE_ARRAY;
430    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1,
431      ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null,
432      now - ttl, now, null);
433
434    int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD;
435    // Feed DCs with empty qualifier past the threshold, then a DF.
436    // The DF must NOT be seeked past -- it must be SKIP'd so the tracker picks it up.
437    qm.setToNewRow(new KeyValue(row1, fam1, e, now, Type.DeleteColumn));
438    for (int i = 0; i < n + 1; i++) {
439      // Empty qualifier DCs should never trigger seek, regardless of threshold
440      assertEquals("DC at i=" + i, MatchCode.SKIP,
441        qm.match(new KeyValue(row1, fam1, e, now - i, Type.DeleteColumn)));
442    }
443    KeyValue df = new KeyValue(row1, fam1, e, now - n - 1, Type.DeleteFamily);
444    KeyValue put = new KeyValue(row1, fam1, col1, now - n - 1, Type.Put, data);
445    // DF must be processed (SKIP), not seeked past
446    assertEquals(MatchCode.SKIP, qm.match(df));
447    // Put in col1 at t=now-3 should be masked by DF@t=now-3
448    MatchCode putCode = qm.match(put);
449    assertEquals(MatchCode.SEEK_NEXT_COL, putCode);
450  }
451
452  /**
453   * DeleteColumn markers for different qualifiers should not accumulate the seek counter. Only
454   * consecutive markers for the same qualifier should trigger seeking.
455   */
456  @Test
457  public void testDeleteColumnDifferentQualifiersDoNotSeek() throws IOException {
458    long now = EnvironmentEdgeManager.currentTime();
459    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1,
460      ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null,
461      now - ttl, now, null);
462
463    // DCs for different qualifiers: counter resets on qualifier change, never seeks
464    qm.setToNewRow(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn));
465    assertEquals(MatchCode.SKIP, qm.match(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn)));
466    assertEquals(MatchCode.SKIP,
467      qm.match(new KeyValue(row1, fam1, col2, now - 1, Type.DeleteColumn)));
468    assertEquals(MatchCode.SKIP,
469      qm.match(new KeyValue(row1, fam1, col3, now - 2, Type.DeleteColumn)));
470    assertEquals(MatchCode.SKIP,
471      qm.match(new KeyValue(row1, fam1, col4, now - 3, Type.DeleteColumn)));
472    assertEquals(MatchCode.SKIP,
473      qm.match(new KeyValue(row1, fam1, col5, now - 4, Type.DeleteColumn)));
474  }
475
476  /**
477   * Delete markers outside the scan's time range (includeDeleteMarker=false) should still
478   * accumulate the seek counter and trigger SEEK_NEXT_COL after the threshold.
479   */
480  @Test
481  public void testSeekOnRangeDeleteOutsideTimeRange() throws IOException {
482    long now = EnvironmentEdgeManager.currentTime();
483    long futureTs = now + 1_000_000;
484    Scan scanWithTimeRange = new Scan(scan).setTimeRange(futureTs, Long.MAX_VALUE);
485
486    UserScanQueryMatcher qm = UserScanQueryMatcher.create(scanWithTimeRange,
487      new ScanInfo(this.conf, fam1, 0, 1, ttl, KeepDeletedCells.FALSE, HConstants.DEFAULT_BLOCKSIZE,
488        0, rowComparator, false),
489      null, now - ttl, now, null);
490
491    int n = NormalUserScanQueryMatcher.SEEK_ON_DELETE_MARKER_THRESHOLD;
492    qm.setToNewRow(new KeyValue(row1, fam1, col1, now, Type.DeleteColumn));
493    // All DCs have timestamps below the time range, so includeDeleteMarker is false.
494    // The seek counter should still accumulate.
495    for (int i = 0; i < n - 1; i++) {
496      assertEquals("DC at i=" + i, MatchCode.SKIP,
497        qm.match(new KeyValue(row1, fam1, col1, now - i, Type.DeleteColumn)));
498    }
499    assertEquals(MatchCode.SEEK_NEXT_COL,
500      qm.match(new KeyValue(row1, fam1, col1, now - n + 1, Type.DeleteColumn)));
501  }
502
503  private UserScanQueryMatcher createDeleteMatcher(KeepDeletedCells keepDeletedCells)
504    throws IOException {
505    long now = EnvironmentEdgeManager.currentTime();
506    return UserScanQueryMatcher.create(scan, new ScanInfo(this.conf, fam1, 0, 1, ttl,
507      keepDeletedCells, HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false), null, now - ttl,
508      now, null);
509  }
510
511  /** First n-1 markers SKIP, n-th triggers SEEK_NEXT_COL. */
512  private void assertSeekAfterThreshold(KeepDeletedCells keepDeletedCells, Type type, int n)
513    throws IOException {
514    long now = EnvironmentEdgeManager.currentTime();
515    UserScanQueryMatcher qm = createDeleteMatcher(keepDeletedCells);
516    boolean familyLevel = type == Type.DeleteFamily || type == Type.DeleteFamilyVersion;
517    byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1;
518    qm.setToNewRow(new KeyValue(row1, fam1, qual, now, type));
519    for (int i = 0; i < n - 1; i++) {
520      assertEquals("Mismatch at index " + i, MatchCode.SKIP,
521        qm.match(new KeyValue(row1, fam1, qual, now - i, type)));
522    }
523    assertEquals("Expected SEEK_NEXT_COL at index " + (n - 1), MatchCode.SEEK_NEXT_COL,
524      qm.match(new KeyValue(row1, fam1, qual, now - n + 1, type)));
525  }
526
527  /** All markers should SKIP regardless of count. */
528  private void assertAllSkip(KeepDeletedCells keepDeletedCells, Type type, int count)
529    throws IOException {
530    long now = EnvironmentEdgeManager.currentTime();
531    UserScanQueryMatcher qm = createDeleteMatcher(keepDeletedCells);
532    boolean familyLevel = type == Type.DeleteFamily || type == Type.DeleteFamilyVersion;
533    byte[] qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1;
534    qm.setToNewRow(new KeyValue(row1, fam1, qual, now, type));
535    for (int i = 0; i < count; i++) {
536      assertEquals("Mismatch at index " + i, MatchCode.SKIP,
537        qm.match(new KeyValue(row1, fam1, qual, now - i, type)));
538    }
539  }
540}