@Override public int compare(final KeyValue left, final KeyValue right) { return rawcomparator.compare( left.getBuffer(), left.getOffset() + KeyValue.ROW_OFFSET, left.getKeyLength(), right.getBuffer(), right.getOffset() + KeyValue.ROW_OFFSET, right.getKeyLength()); }
/** * Add key/value to file. Keys must be added in an order that agrees with the Comparator passed on * construction. * * @param kv KeyValue to add. Cannot be empty nor null. * @throws IOException */ @Override public void append(final KeyValue kv) throws IOException { append( kv.getMemstoreTS(), kv.getBuffer(), kv.getKeyOffset(), kv.getKeyLength(), kv.getBuffer(), kv.getValueOffset(), kv.getValueLength()); this.maxMemstoreTS = Math.max(this.maxMemstoreTS, kv.getMemstoreTS()); }
/** * Add key/value to file. Keys must be added in an order that agrees with the Comparator passed on * construction. * * @param kv KeyValue to add. Cannot be empty nor null. * @throws IOException */ @Override public void append(final KeyValue kv) throws IOException { byte[] key = kv.getBuffer(); int koffset = kv.getKeyOffset(); int klength = kv.getKeyLength(); byte[] value = kv.getValueArray(); int voffset = kv.getValueOffset(); int vlength = kv.getValueLength(); boolean dupKey = checkKey(key, koffset, klength); checkValue(value, voffset, vlength); if (!dupKey) { checkBlockBoundary(); } if (!fsBlockWriter.isWriting()) newBlock(); fsBlockWriter.write(kv); totalKeyLength += klength; totalValueLength += vlength; // Are we the first key in this block? if (firstKeyInBlock == null) { // Copy the key. firstKeyInBlock = new byte[klength]; System.arraycopy(key, koffset, firstKeyInBlock, 0, klength); } lastKeyBuffer = key; lastKeyOffset = koffset; lastKeyLength = klength; entryCount++; this.maxMemstoreTS = Math.max(this.maxMemstoreTS, kv.getMvccVersion()); }
@SuppressWarnings("deprecation") @Override protected void writeToBuffer(MappedByteBuffer buffer, Tuple e) { KeyValue kv = KeyValueUtil.ensureKeyValue(e.getValue(0)); buffer.putInt(kv.getLength() + Bytes.SIZEOF_INT); buffer.putInt(kv.getLength()); buffer.put(kv.getBuffer(), kv.getOffset(), kv.getLength()); }
@Override public void examine(SkipScanFilter skipper) { KeyValue kv = KeyValue.createFirstOnRow(rowkey); skipper.reset(); assertFalse(skipper.filterAllRemaining()); assertFalse(skipper.filterRowKey(kv.getBuffer(), kv.getRowOffset(), kv.getRowLength())); assertEquals(kv.toString(), ReturnCode.INCLUDE, skipper.filterKeyValue(kv)); }
@Override public void examine(SkipScanFilter skipper) { KeyValue kv = KeyValue.createFirstOnRow(rowkey); skipper.reset(); assertFalse(skipper.filterAllRemaining()); assertFalse(skipper.filterRowKey(kv.getBuffer(), kv.getRowOffset(), kv.getRowLength())); assertEquals(ReturnCode.SEEK_NEXT_USING_HINT, skipper.filterKeyValue(kv)); assertEquals(KeyValue.createFirstOnRow(hint), skipper.getNextKeyHint(kv)); }
static boolean reseekAtOrAfter(HFileScanner s, KeyValue k) throws IOException { // This function is similar to seekAtOrAfter function int result = s.reseekTo(k.getBuffer(), k.getKeyOffset(), k.getKeyLength()); if (result <= 0) { return true; } else { // passed KV is larger than current KV in file, if there is a next // it is after, if not then this scanner is done. return s.next(); } }
@Test public void testNextOnSample() throws IOException { List<KeyValue> sampleKv = generator.generateTestKeyValues(NUMBER_OF_KV, includesTags); for (DataBlockEncoding encoding : DataBlockEncoding.values()) { // Off heap block data support not added for PREFIX_TREE DBE yet. // TODO remove this once support is added. HBASE-12298 if (this.useOffheapData && encoding == DataBlockEncoding.PREFIX_TREE) continue; if (encoding.getEncoder() == null) { continue; } DataBlockEncoder encoder = encoding.getEncoder(); ByteBuffer encodedBuffer = encodeKeyValues( encoding, sampleKv, getEncodingContext(Compression.Algorithm.NONE, encoding), this.useOffheapData); HFileContext meta = new HFileContextBuilder() .withHBaseCheckSum(false) .withIncludesMvcc(includesMemstoreTS) .withIncludesTags(includesTags) .withCompression(Compression.Algorithm.NONE) .build(); DataBlockEncoder.EncodedSeeker seeker = encoder.createSeeker( CellComparator.COMPARATOR, encoder.newDataBlockDecodingContext(meta)); seeker.setCurrentBuffer(new SingleByteBuff(encodedBuffer)); int i = 0; do { KeyValue expectedKeyValue = sampleKv.get(i); Cell cell = seeker.getCell(); if (CellComparator.COMPARATOR.compareKeyIgnoresMvcc(expectedKeyValue, cell) != 0) { int commonPrefix = CellUtil.findCommonPrefixInFlatKey(expectedKeyValue, cell, false, true); fail( String.format( "next() produces wrong results " + "encoder: %s i: %d commonPrefix: %d" + "\n expected %s\n actual %s", encoder.toString(), i, commonPrefix, Bytes.toStringBinary( expectedKeyValue.getBuffer(), expectedKeyValue.getKeyOffset(), expectedKeyValue.getKeyLength()), CellUtil.toString(cell, false))); } i++; } while (seeker.next()); } }
/** * Check if the specified KeyValue buffer has been deleted by a previously seen delete. * * @param kv * @param ds * @return True is the specified KeyValue is deleted, false if not */ public boolean isDeleted(final KeyValue kv, final NavigableSet<KeyValue> ds) { if (deletes == null || deletes.isEmpty()) return false; for (KeyValue d : ds) { long kvts = kv.getTimestamp(); long dts = d.getTimestamp(); if (d.isDeleteFamily()) { if (kvts <= dts) return true; continue; } // Check column int ret = Bytes.compareTo( kv.getBuffer(), kv.getQualifierOffset(), kv.getQualifierLength(), d.getBuffer(), d.getQualifierOffset(), d.getQualifierLength()); if (ret <= -1) { // This delete is for an earlier column. continue; } else if (ret >= 1) { // Beyond this kv. break; } // Check Timestamp if (kvts > dts) return false; // Check Type switch (KeyValue.Type.codeToType(d.getType())) { case Delete: return kvts == dts; case DeleteColumn: return true; default: continue; } } return false; }
boolean isTargetTable(final KeyValue kv) { if (!metaregion) return true; // Compare start of keys row. Compare including delimiter. Saves having // to calculate where tablename ends in the candidate kv. return Bytes.compareTo( this.targetkey.getBuffer(), this.rowoffset, this.tablenamePlusDelimiterLength, kv.getBuffer(), kv.getRowOffset(), this.tablenamePlusDelimiterLength) == 0; }
/** * Binary search for latest column value without allocating memory in the process * * @param r * @param searchTerm * @return */ public static KeyValue getColumnLatest(List<KeyValue> kvs, KeyValue searchTerm) { if (kvs.size() == 0) { return null; } // pos === ( -(insertion point) - 1) int pos = Collections.binarySearch(kvs, searchTerm, KeyValue.COMPARATOR); // never will exact match if (pos < 0) { pos = (pos + 1) * -1; // pos is now insertion point } if (pos == kvs.size()) { return null; // doesn't exist } KeyValue kv = kvs.get(pos); if (Bytes.compareTo( kv.getBuffer(), kv.getFamilyOffset(), kv.getFamilyLength(), searchTerm.getBuffer(), searchTerm.getFamilyOffset(), searchTerm.getFamilyLength()) != 0) { return null; } if (Bytes.compareTo( kv.getBuffer(), kv.getQualifierOffset(), kv.getQualifierLength(), searchTerm.getBuffer(), searchTerm.getQualifierOffset(), searchTerm.getQualifierLength()) != 0) { return null; } return kv; }
/** * @param s * @param k * @return * @throws IOException */ public static boolean seekAtOrAfter(HFileScanner s, KeyValue k) throws IOException { int result = s.seekTo(k.getBuffer(), k.getKeyOffset(), k.getKeyLength()); if (result < 0) { // Passed KV is smaller than first KV in file, work from start of file return s.seekTo(); } else if (result > 0) { // Passed KV is larger than current KV in file, if there is a next // it is the "after", if not then this scanner is done. return s.next(); } // Seeked to the exact key return true; }
/** Binary search for latest column value without allocating memory in the process */ public static KeyValue getColumnLatest(List<KeyValue> kvs, byte[] family, byte[] qualifier) { KeyValue kv = kvs.get(0); return KeyValueUtil.getColumnLatest( kvs, kv.getBuffer(), kv.getRowOffset(), kv.getRowLength(), family, 0, family.length, qualifier, 0, qualifier.length); }
/** * Convert list of KeyValues to byte buffer. * * @param keyValues list of KeyValues to be converted. * @return buffer with content from key values */ public static ByteBuffer convertKvToByteBuffer( List<KeyValue> keyValues, boolean includesMemstoreTS) { int totalSize = 0; for (KeyValue kv : keyValues) { totalSize += kv.getLength(); if (includesMemstoreTS) { totalSize += WritableUtils.getVIntSize(kv.getMvccVersion()); } } ByteBuffer result = ByteBuffer.allocate(totalSize); for (KeyValue kv : keyValues) { result.put(kv.getBuffer(), kv.getOffset(), kv.getLength()); if (includesMemstoreTS) { ByteBufferUtils.writeVLong(result, kv.getMvccVersion()); } } return result; }
/** * @param c * @param kv Presume first on row: i.e. empty column, maximum timestamp and a type of Type.Maximum * @param ttl Time to live in ms for this Store * @param metaregion True if this is .META. or -ROOT- region. */ GetClosestRowBeforeTracker( final KVComparator c, final KeyValue kv, final long ttl, final boolean metaregion) { super(); this.metaregion = metaregion; this.targetkey = kv; // If we are in a metaregion, then our table name is the prefix on the // targetkey. this.rowoffset = kv.getRowOffset(); int l = -1; if (metaregion) { l = KeyValue.getDelimiter(kv.getBuffer(), rowoffset, kv.getRowLength(), HConstants.DELIMITER) - this.rowoffset; } this.tablenamePlusDelimiterLength = metaregion ? l + 1 : -1; this.oldestts = System.currentTimeMillis() - ttl; this.kvcomparator = c; KeyValue.RowComparator rc = new KeyValue.RowComparator(this.kvcomparator); this.deletes = new TreeMap<KeyValue, NavigableSet<KeyValue>>(rc); }
@Override public MetaDataMutationResult updateIndexState( List<Mutation> tableMetadata, String parentTableName) throws SQLException { byte[][] rowKeyMetadata = new byte[3][]; SchemaUtil.getVarChars(tableMetadata.get(0).getRow(), rowKeyMetadata); KeyValue newKV = tableMetadata.get(0).getFamilyMap().get(TABLE_FAMILY_BYTES).get(0); PIndexState newState = PIndexState.fromSerializedValue(newKV.getBuffer()[newKV.getValueOffset()]); String schemaName = Bytes.toString(rowKeyMetadata[PhoenixDatabaseMetaData.SCHEMA_NAME_INDEX]); String indexName = Bytes.toString(rowKeyMetadata[PhoenixDatabaseMetaData.TABLE_NAME_INDEX]); String indexTableName = SchemaUtil.getTableName(schemaName, indexName); PTable index = metaData.getTable(indexTableName); index = PTableImpl.makePTable( index, newState == PIndexState.USABLE ? PIndexState.ACTIVE : newState == PIndexState.UNUSABLE ? PIndexState.INACTIVE : newState); return new MetaDataMutationResult(MutationCode.TABLE_ALREADY_EXISTS, 0, index); }
/** * Write out a split reference. Package local so it doesnt leak out of regionserver. * * @param hri {@link HRegionInfo} of the destination * @param familyName Column Family Name * @param f File to split. * @param splitRow Split Row * @param top True if we are referring to the top half of the hfile. * @return Path to created reference. * @throws IOException */ Path splitStoreFile( final HRegionInfo hri, final String familyName, final StoreFile f, final byte[] splitRow, final boolean top) throws IOException { // Check whether the split row lies in the range of the store file // If it is outside the range, return directly. if (!isIndexTable()) { if (top) { // check if larger than last key. KeyValue splitKey = KeyValue.createFirstOnRow(splitRow); byte[] lastKey = f.createReader().getLastKey(); // If lastKey is null means storefile is empty. if (lastKey == null) return null; if (f.getReader() .getComparator() .compareFlatKey( splitKey.getBuffer(), splitKey.getKeyOffset(), splitKey.getKeyLength(), lastKey, 0, lastKey.length) > 0) { return null; } } else { // check if smaller than first key KeyValue splitKey = KeyValue.createLastOnRow(splitRow); byte[] firstKey = f.createReader().getFirstKey(); // If firstKey is null means storefile is empty. if (firstKey == null) return null; if (f.getReader() .getComparator() .compareFlatKey( splitKey.getBuffer(), splitKey.getKeyOffset(), splitKey.getKeyLength(), firstKey, 0, firstKey.length) < 0) { return null; } } f.getReader().close(true); } Path splitDir = new Path(getSplitsDir(hri), familyName); // A reference to the bottom half of the hsf store file. Reference r = top ? Reference.createTopReference(splitRow) : Reference.createBottomReference(splitRow); // Add the referred-to regions name as a dot separated suffix. // See REF_NAME_REGEX regex above. The referred-to regions name is // up in the path of the passed in <code>f</code> -- parentdir is family, // then the directory above is the region name. String parentRegionName = regionInfo.getEncodedName(); // Write reference with same file id only with the other region name as // suffix and into the new region location (under same family). Path p = new Path(splitDir, f.getPath().getName() + "." + parentRegionName); return r.write(fs, p); }
/** * Override the preAppend for checkAndPut and checkAndDelete, as we need the ability to a) set the * TimeRange for the Get being done and b) return something back to the client to indicate * success/failure */ @SuppressWarnings("deprecation") @Override public Result preAppend( final ObserverContext<RegionCoprocessorEnvironment> e, final Append append) throws IOException { byte[] opBuf = append.getAttribute(OPERATION_ATTRIB); if (opBuf == null) { return null; } Op op = Op.values()[opBuf[0]]; long clientTimestamp = HConstants.LATEST_TIMESTAMP; byte[] clientTimestampBuf = append.getAttribute(MAX_TIMERANGE_ATTRIB); if (clientTimestampBuf != null) { clientTimestamp = Bytes.toLong(clientTimestampBuf); } boolean hadClientTimestamp = (clientTimestamp != HConstants.LATEST_TIMESTAMP); if (hadClientTimestamp) { // Prevent race condition of creating two sequences at the same timestamp // by looking for a sequence at or after the timestamp at which it'll be // created. if (op == Op.CREATE_SEQUENCE) { clientTimestamp++; } } else { clientTimestamp = EnvironmentEdgeManager.currentTimeMillis(); clientTimestampBuf = Bytes.toBytes(clientTimestamp); } RegionCoprocessorEnvironment env = e.getEnvironment(); // We need to set this to prevent region.append from being called e.bypass(); e.complete(); HRegion region = env.getRegion(); byte[] row = append.getRow(); region.startRegionOperation(); try { Integer lid = region.getLock(null, row, true); try { KeyValue keyValue = append.getFamilyMap().values().iterator().next().iterator().next(); byte[] family = keyValue.getFamily(); byte[] qualifier = keyValue.getQualifier(); Get get = new Get(row); get.setTimeRange(MetaDataProtocol.MIN_TABLE_TIMESTAMP, clientTimestamp); get.addColumn(family, qualifier); Result result = region.get(get); if (result.isEmpty()) { if (op == Op.DROP_SEQUENCE || op == Op.RESET_SEQUENCE) { return getErrorResult( row, clientTimestamp, SQLExceptionCode.SEQUENCE_UNDEFINED.getErrorCode()); } } else { if (op == Op.CREATE_SEQUENCE) { return getErrorResult( row, clientTimestamp, SQLExceptionCode.SEQUENCE_ALREADY_EXIST.getErrorCode()); } } Mutation m = null; switch (op) { case RESET_SEQUENCE: KeyValue currentValueKV = result.raw()[0]; long expectedValue = PDataType.LONG .getCodec() .decodeLong(append.getAttribute(CURRENT_VALUE_ATTRIB), 0, null); long value = PDataType.LONG .getCodec() .decodeLong(currentValueKV.getBuffer(), currentValueKV.getValueOffset(), null); // Timestamp should match exactly, or we may have the wrong sequence if (expectedValue != value || currentValueKV.getTimestamp() != clientTimestamp) { return new Result( Collections.singletonList( KeyValueUtil.newKeyValue( row, PhoenixDatabaseMetaData.SEQUENCE_FAMILY_BYTES, QueryConstants.EMPTY_COLUMN_BYTES, currentValueKV.getTimestamp(), ByteUtil.EMPTY_BYTE_ARRAY))); } m = new Put(row, currentValueKV.getTimestamp()); m.getFamilyMap().putAll(append.getFamilyMap()); break; case DROP_SEQUENCE: m = new Delete(row, clientTimestamp, null); break; case CREATE_SEQUENCE: m = new Put(row, clientTimestamp); m.getFamilyMap().putAll(append.getFamilyMap()); break; } if (!hadClientTimestamp) { for (List<KeyValue> kvs : m.getFamilyMap().values()) { for (KeyValue kv : kvs) { kv.updateLatestStamp(clientTimestampBuf); } } } @SuppressWarnings("unchecked") Pair<Mutation, Integer>[] mutations = new Pair[1]; mutations[0] = new Pair<Mutation, Integer>(m, lid); region.batchMutate(mutations); long serverTimestamp = MetaDataUtil.getClientTimeStamp(m); // Return result with single KeyValue. The only piece of information // the client cares about is the timestamp, which is the timestamp of // when the mutation was actually performed (useful in the case of . return new Result( Collections.singletonList( KeyValueUtil.newKeyValue( row, PhoenixDatabaseMetaData.SEQUENCE_FAMILY_BYTES, QueryConstants.EMPTY_COLUMN_BYTES, serverTimestamp, SUCCESS_VALUE))); } finally { region.releaseRowLock(lid); } } catch (Throwable t) { ServerUtil.throwIOException("Increment of sequence " + Bytes.toStringBinary(row), t); return null; // Impossible } finally { region.closeRegionOperation(); } }
@Override public ReturnCode filterKeyValue(KeyValue kv) { return navigate(kv.getBuffer(), kv.getRowOffset(), kv.getRowLength(), Terminate.AFTER); }
/** * Use PreIncrement hook of BaseRegionObserver to overcome deficiencies in Increment * implementation (HBASE-10254): 1) Lack of recognition and identification of when the key value * to increment doesn't exist 2) Lack of the ability to set the timestamp of the updated key * value. Works the same as existing region.increment(), except assumes there is a single column * to increment and uses Phoenix LONG encoding. * * @author jtaylor * @since 3.0.0 */ @Override public Result preIncrement( final ObserverContext<RegionCoprocessorEnvironment> e, final Increment increment) throws IOException { RegionCoprocessorEnvironment env = e.getEnvironment(); // We need to set this to prevent region.increment from being called e.bypass(); e.complete(); HRegion region = env.getRegion(); byte[] row = increment.getRow(); TimeRange tr = increment.getTimeRange(); region.startRegionOperation(); try { Integer lid = region.getLock(null, row, true); try { long maxTimestamp = tr.getMax(); if (maxTimestamp == HConstants.LATEST_TIMESTAMP) { maxTimestamp = EnvironmentEdgeManager.currentTimeMillis(); tr = new TimeRange(tr.getMin(), maxTimestamp); } Get get = new Get(row); get.setTimeRange(tr.getMin(), tr.getMax()); for (Map.Entry<byte[], NavigableMap<byte[], Long>> entry : increment.getFamilyMap().entrySet()) { byte[] cf = entry.getKey(); for (byte[] cq : entry.getValue().keySet()) { get.addColumn(cf, cq); } } Result result = region.get(get); if (result.isEmpty()) { return getErrorResult( row, maxTimestamp, SQLExceptionCode.SEQUENCE_UNDEFINED.getErrorCode()); } KeyValue currentValueKV = Sequence.getCurrentValueKV(result); KeyValue incrementByKV = Sequence.getIncrementByKV(result); KeyValue cacheSizeKV = Sequence.getCacheSizeKV(result); long value = PDataType.LONG .getCodec() .decodeLong(currentValueKV.getBuffer(), currentValueKV.getValueOffset(), null); long incrementBy = PDataType.LONG .getCodec() .decodeLong(incrementByKV.getBuffer(), incrementByKV.getValueOffset(), null); int cacheSize = PDataType.INTEGER .getCodec() .decodeInt(cacheSizeKV.getBuffer(), cacheSizeKV.getValueOffset(), null); value += incrementBy * cacheSize; byte[] valueBuffer = new byte[PDataType.LONG.getByteSize()]; PDataType.LONG.getCodec().encodeLong(value, valueBuffer, 0); Put put = new Put(row, currentValueKV.getTimestamp()); // Hold timestamp constant for sequences, so that clients always only see the latest value // regardless of when they connect. KeyValue newCurrentValueKV = KeyValueUtil.newKeyValue( row, currentValueKV.getFamily(), currentValueKV.getQualifier(), currentValueKV.getTimestamp(), valueBuffer); put.add(newCurrentValueKV); @SuppressWarnings("unchecked") Pair<Mutation, Integer>[] mutations = new Pair[1]; mutations[0] = new Pair<Mutation, Integer>(put, lid); region.batchMutate(mutations); return Sequence.replaceCurrentValueKV(result, newCurrentValueKV); } finally { region.releaseRowLock(lid); } } catch (Throwable t) { ServerUtil.throwIOException("Increment of sequence " + Bytes.toStringBinary(row), t); return null; // Impossible } finally { region.closeRegionOperation(); } }
/** * Pretend we have done a seek but don't do it yet, if possible. The hope is that we find * requested columns in more recent files and won't have to seek in older files. Creates a fake * key/value with the given row/column and the highest (most recent) possible timestamp we might * get from this file. When users of such "lazy scanner" need to know the next KV precisely (e.g. * when this scanner is at the top of the heap), they run {@link #enforceSeek()}. * * <p>Note that this function does guarantee that the current KV of this scanner will be advanced * to at least the given KV. Because of this, it does have to do a real seek in cases when the * seek timestamp is older than the highest timestamp of the file, e.g. when we are trying to seek * to the next row/column and use OLDEST_TIMESTAMP in the seek key. */ @Override public boolean requestSeek(KeyValue kv, boolean forward, boolean useBloom) throws IOException { if (kv.getFamilyLength() == 0) { useBloom = false; } boolean haveToSeek = true; if (useBloom) { // check ROWCOL Bloom filter first. if (reader.getBloomFilterType() == StoreFile.BloomType.ROWCOL) { haveToSeek = reader.passesGeneralBloomFilter( kv.getBuffer(), kv.getRowOffset(), kv.getRowLength(), kv.getBuffer(), kv.getQualifierOffset(), kv.getQualifierLength()); } else if (this.matcher != null && !matcher.hasNullColumnInQuery() && kv.isDeleteFamily()) { // if there is no such delete family kv in the store file, // then no need to seek. haveToSeek = reader.passesDeleteFamilyBloomFilter( kv.getBuffer(), kv.getRowOffset(), kv.getRowLength()); } } delayedReseek = forward; delayedSeekKV = kv; if (haveToSeek) { // This row/column might be in this store file (or we did not use the // Bloom filter), so we still need to seek. realSeekDone = false; long maxTimestampInFile = reader.getMaxTimestamp(); long seekTimestamp = kv.getTimestamp(); if (seekTimestamp > maxTimestampInFile) { // Create a fake key that is not greater than the real next key. // (Lower timestamps correspond to higher KVs.) // To understand this better, consider that we are asked to seek to // a higher timestamp than the max timestamp in this file. We know that // the next point when we have to consider this file again is when we // pass the max timestamp of this file (with the same row/column). cur = kv.createFirstOnRowColTS(maxTimestampInFile); } else { // This will be the case e.g. when we need to seek to the next // row/column, and we don't know exactly what they are, so we set the // seek key's timestamp to OLDEST_TIMESTAMP to skip the rest of this // row/column. enforceSeek(); } return cur != null; } // Multi-column Bloom filter optimization. // Create a fake key/value, so that this scanner only bubbles up to the top // of the KeyValueHeap in StoreScanner after we scanned this row/column in // all other store files. The query matcher will then just skip this fake // key/value and the store scanner will progress to the next column. This // is obviously not a "real real" seek, but unlike the fake KV earlier in // this method, we want this to be propagated to ScanQueryMatcher. cur = kv.createLastOnRowCol(); realSeekDone = true; return true; }