|
18 | 18 | package org.apache.hadoop.hbase.regionserver.querymatcher; |
19 | 19 |
|
20 | 20 | import static org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode.INCLUDE; |
| 21 | +import static org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode.SEEK_NEXT_COL; |
21 | 22 | import static org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode.SKIP; |
22 | 23 | import static org.junit.Assert.assertEquals; |
23 | 24 |
|
@@ -75,6 +76,121 @@ public void testMatch_PartialRangeDropDeletes() throws Exception { |
75 | 76 | testDropDeletes(row2, row3, new byte[][] { row1, row1 }, INCLUDE, INCLUDE); |
76 | 77 | } |
77 | 78 |
|
| 79 | + /** |
| 80 | + * Test redundant delete marker handling with COMPACT_RETAIN_DELETES. Cells are auto-generated |
| 81 | + * from the given types with decrementing timestamps. |
| 82 | + */ |
| 83 | + @Test |
| 84 | + public void testSkipsRedundantDeleteMarkers() throws IOException { |
| 85 | + // Interleaved DeleteColumn + Put. First DC included, put triggers SEEK_NEXT_COL. |
| 86 | + assertRetainDeletes(new Type[] { Type.DeleteColumn, Type.Put, Type.DeleteColumn }, INCLUDE, |
| 87 | + SEEK_NEXT_COL); |
| 88 | + |
| 89 | + // Contiguous DeleteColumn. First included, rest redundant. |
| 90 | + assertRetainDeletes(new Type[] { Type.DeleteColumn, Type.DeleteColumn, Type.DeleteColumn }, |
| 91 | + INCLUDE, SEEK_NEXT_COL, SEEK_NEXT_COL); |
| 92 | + |
| 93 | + // Contiguous DeleteFamily. First included, rest redundant. |
| 94 | + assertRetainDeletes(new Type[] { Type.DeleteFamily, Type.DeleteFamily, Type.DeleteFamily }, |
| 95 | + INCLUDE, SEEK_NEXT_COL, SEEK_NEXT_COL); |
| 96 | + |
| 97 | + // DF + DFV interleaved. DF included, DFV redundant (SKIP because empty qualifier), |
| 98 | + // older DF redundant (SEEK_NEXT_COL), older DFV redundant (SKIP). |
| 99 | + assertRetainDeletes(new Type[] { Type.DeleteFamily, Type.DeleteFamilyVersion, Type.DeleteFamily, |
| 100 | + Type.DeleteFamilyVersion }, INCLUDE, SKIP, SEEK_NEXT_COL, SKIP); |
| 101 | + |
| 102 | + // Delete (version) covered by DeleteColumn. |
| 103 | + assertRetainDeletes(new Type[] { Type.DeleteColumn, Type.Delete, Type.Delete, Type.Delete }, |
| 104 | + INCLUDE, SEEK_NEXT_COL, SEEK_NEXT_COL, SEEK_NEXT_COL); |
| 105 | + |
| 106 | + // KEEP_DELETED_CELLS=TRUE: all markers retained. |
| 107 | + assertRetainDeletes(KeepDeletedCells.TRUE, |
| 108 | + new Type[] { Type.DeleteColumn, Type.DeleteColumn, Type.DeleteColumn }, INCLUDE, INCLUDE, |
| 109 | + INCLUDE); |
| 110 | + } |
| 111 | + |
| 112 | + /** |
| 113 | + * Redundant column-level deletes with empty qualifier must not seek past a subsequent |
| 114 | + * DeleteFamily. getKeyForNextColumn treats empty qualifier as "no column" and returns |
| 115 | + * SEEK_NEXT_ROW, which would skip the DF and all remaining cells in the row. |
| 116 | + */ |
| 117 | + @Test |
| 118 | + public void testEmptyQualifierDeleteDoesNotSkipDeleteFamily() throws IOException { |
| 119 | + byte[] emptyQualifier = HConstants.EMPTY_BYTE_ARRAY; |
| 120 | + |
| 121 | + // DC(empty) + DC(empty) redundant + DF must still be reachable. |
| 122 | + assertRetainDeletes(emptyQualifier, |
| 123 | + new Type[] { Type.DeleteColumn, Type.DeleteColumn, Type.DeleteFamily }, INCLUDE, SKIP, |
| 124 | + INCLUDE); |
| 125 | + |
| 126 | + // DC(empty) + Delete(empty) redundant + DF must still be reachable. |
| 127 | + assertRetainDeletes(emptyQualifier, |
| 128 | + new Type[] { Type.DeleteColumn, Type.Delete, Type.DeleteFamily }, INCLUDE, SKIP, INCLUDE); |
| 129 | + } |
| 130 | + |
| 131 | + private void assertRetainDeletes(Type[] types, MatchCode... expected) throws IOException { |
| 132 | + assertRetainDeletes(KeepDeletedCells.FALSE, types, expected); |
| 133 | + } |
| 134 | + |
| 135 | + private void assertRetainDeletes(byte[] qualifier, Type[] types, MatchCode... expected) |
| 136 | + throws IOException { |
| 137 | + assertRetainDeletes(KeepDeletedCells.FALSE, qualifier, types, expected); |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Build cells from the given types with decrementing timestamps (same ts for adjacent |
| 142 | + * family-level and column-level types at the same position). Family-level types (DeleteFamily, |
| 143 | + * DeleteFamilyVersion) use empty qualifier; others use col1. |
| 144 | + */ |
| 145 | + private void assertRetainDeletes(KeepDeletedCells keepDeletedCells, Type[] types, |
| 146 | + MatchCode... expected) throws IOException { |
| 147 | + assertRetainDeletes(keepDeletedCells, null, types, expected); |
| 148 | + } |
| 149 | + |
| 150 | + /** |
| 151 | + * Build cells from the given types with decrementing timestamps. If qualifier is null, |
| 152 | + * family-level types use empty qualifier and others use col1. If qualifier is specified, all |
| 153 | + * types use that qualifier. |
| 154 | + */ |
| 155 | + private void assertRetainDeletes(KeepDeletedCells keepDeletedCells, byte[] qualifier, |
| 156 | + Type[] types, MatchCode... expected) throws IOException { |
| 157 | + long now = EnvironmentEdgeManager.currentTime(); |
| 158 | + ScanInfo scanInfo = new ScanInfo(this.conf, fam1, 0, 1, ttl, keepDeletedCells, |
| 159 | + HConstants.DEFAULT_BLOCKSIZE, 0, rowComparator, false); |
| 160 | + CompactionScanQueryMatcher qm = CompactionScanQueryMatcher.create(scanInfo, |
| 161 | + ScanType.COMPACT_RETAIN_DELETES, 0L, PrivateConstants.OLDEST_TIMESTAMP, |
| 162 | + PrivateConstants.OLDEST_TIMESTAMP, now, null, null, null); |
| 163 | + qm.setToNewRow(KeyValueUtil.createFirstOnRow(row1)); |
| 164 | + |
| 165 | + long ts = now; |
| 166 | + List<MatchCode> actual = new ArrayList<>(expected.length); |
| 167 | + for (int i = 0; i < types.length; i++) { |
| 168 | + byte[] qual; |
| 169 | + if (qualifier != null) { |
| 170 | + qual = qualifier; |
| 171 | + } else { |
| 172 | + boolean familyLevel = types[i] == Type.DeleteFamily || types[i] == Type.DeleteFamilyVersion; |
| 173 | + qual = familyLevel ? HConstants.EMPTY_BYTE_ARRAY : col1; |
| 174 | + } |
| 175 | + KeyValue kv = types[i] == Type.Put |
| 176 | + ? new KeyValue(row1, fam1, qual, ts, types[i], data) |
| 177 | + : new KeyValue(row1, fam1, qual, ts, types[i]); |
| 178 | + actual.add(qm.match(kv)); |
| 179 | + if (actual.size() >= expected.length) { |
| 180 | + break; |
| 181 | + } |
| 182 | + // Decrement ts for next cell, but keep same ts when the next type has lower type code |
| 183 | + // at the same logical position (e.g. DF then DFV at the same timestamp). |
| 184 | + if (i + 1 < types.length && types[i + 1].getCode() < types[i].getCode()) { |
| 185 | + continue; |
| 186 | + } |
| 187 | + ts--; |
| 188 | + } |
| 189 | + for (int i = 0; i < expected.length; i++) { |
| 190 | + assertEquals("Mismatch at index " + i, expected[i], actual.get(i)); |
| 191 | + } |
| 192 | + } |
| 193 | + |
78 | 194 | private void testDropDeletes(byte[] from, byte[] to, byte[][] rows, MatchCode... expected) |
79 | 195 | throws IOException { |
80 | 196 | long now = EnvironmentEdgeManager.currentTime(); |
|
0 commit comments