Repository navigation
feat: add RocksIterator.stream(Mapper, Mapper) for internal iteration - #186
Conversation
Adds KeyValue<K, V>, a mapped key/value pair, and a package-private RocksIteratorSpliterator that wires tryAdvance to the iterator's existing key(Mapper)/value(Mapper) scoped reads. stream() returns a Stream<KeyValue<K, V>> whose close() closes the iterator, even on early termination, so a caller can use it directly in try-with- resources instead of the raw seekToFirst()/isValid()/next() loop. Per discussion on dfa1#181: keeps Mapper<K>/Mapper<V> rather than adding new KeyMapper/ValueMapper interfaces, and does not add a byte[]-based overload since the mapped version already covers that case with no forced intermediate copy.
|
|
||
| // When | ||
| List<KeyValue<String, String>> entries; | ||
| try (RocksIterator it = db.newIterator(); |
There was a problem hiding this comment.
what if we merge the 2 methods into one? db.newIteratorStream(keyMapper, valueMapper)
There was a problem hiding this comment.
Good call, added newIteratorStream(keyMapper, valueMapper) as a default method on RocksDBReadOperations that delegates to newIterator().stream(...). Kept the two-arg stream() too since it's still useful when you already have an iterator open, but this is the one most callers will want.
Per dfa1's review on this PR: merges newIterator() + stream() into a single call on RocksDBReadOperations, right next to newIterator(). It just delegates to newIterator().stream(keyMapper, valueMapper), so it keeps the same close-on-stream-close behavior with no extra code.
|
Thanks for this — the shape is right: reusing Three things before merge. 1.
|
| scan length | fresh iterator per scan | reused iterator | overhead |
|---|---|---|---|
| 1 entry | 1.06 µs | 0.55 µs | 1.93x |
| 10 entries | 1.68 µs | 1.11 µs | 1.51x |
| 1000 entries | 64.8 µs | 62.5 µs | 1.04x |
Noise on a full table scan; 1.5–1.9x on the short repeated scans a Stream API invites (.limit(10), prefix lookup in a loop). It's also the one-shot shape AGENTS.md already rejected once, for ReadBatch: "There is deliberately no one-shot multiGet()."
Dropping onClose — and newIteratorStream with it — lets the caller keep the handle:
try (var it = db.newIterator()) {
var head = it.stream(k, v).limit(10).toList();
it.seek(prefix);
var window = it.stream(k, v).takeWhile(p -> ...).toList();
}On the obvious safety worry: I checked, and closing the iterator then using the stream throws IllegalStateException, not a crash — NativeObject.close() nulls the pointer atomically and every tryAdvance step goes through ptr(). That holds with or without onClose, so the ownership transfer isn't buying safety here, only preventing reuse.
3. The javadoc contradicts itself
Returns a
Streamover the remaining entries from the current position … Positions itself withseekToFirst()on first use
Both can't be true. It's always a full scan, which also means it.seek(prefix) followed by .stream(...) is impossible — probably the most common reason to want this. Dropping the started flag and the seek fixes the doc and unblocks range scans in one go.
Also needs a CHANGELOG.md [Unreleased] entry and a Stream row in the Iterators table in docs/reference.md.
Worth noting the fix for all three is a net deletion — smaller than what's here now. Happy to talk through any of it.
stream() silently truncated instead of throwing when isValid() went false from a background error, not just end-of-range. checkError() is now called in that case, matching the manual seekToFirst/isValid/next loop's own checkError()-after-the-loop convention. Drop the forced seekToFirst() and onClose(this::close): stream() now has the same positioning contract as next() (seek first, same as the manual loop) and never closes the iterator, so a seek() + stream() + reseek() + stream() sequence works and one iterator can be reused across short, repeated scans instead of paying for a fresh iterator per call. Remove newIteratorStream(), which only existed to hide that per-call cost. Update CHANGELOG.md and the docs/reference.md Iterators table.
|
Thanks for the thorough review, especially the benchmark. Fixed all three: checkError() now runs when the range ends, stream() no longer seeks or closes the iterator so it's reusable across reseeks, and the javadoc matches that. Added a test for the seek-then-restream case your example showed. CHANGELOG.md and the docs/reference.md Iterators table are updated too. |
|
Thanks for your contribution @arpitJ-dev! |
Fixes #181.
Adds
KeyValue<K, V>and a package-privateRocksIteratorSpliteratorwired to the existingkey(Mapper)/value(Mapper)scoped reads.RocksIterator.stream(Mapper<K>, Mapper<V>)returns aStream<KeyValue<K, V>>whoseclose()closes the iterator, including on early termination (e.g..limit(n)), so it works directly in try-with-resources.Per discussion on #181: kept
Mapper<K>/Mapper<V>instead of newKeyMapper/ValueMapperinterfaces, and left out abyte[]overload since the mapped version already covers that with no forced copy.Tested locally:
./mvnw test -pl core -am, full suite green (including 5 new tests inRocksIteratorTest).