Skip to content

feat: seekable reader#530

Draft
splix wants to merge 3 commits intoapache:mainfrom
splix:feat/reader-seek
Draft

feat: seekable reader#530
splix wants to merge 3 commits intoapache:mainfrom
splix:feat/reader-seek

Conversation

@splix
Copy link
Copy Markdown
Contributor

@splix splix commented Apr 5, 2026

Added a feature to seek to a particular block when reading an Avro file.

The Reader now provides the current Block position and for a Seek'able input it can seek to the specific position, assuming it's a valid start position of a block.

Copilot AI review requested due to automatic review settings April 5, 2026 23:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds seek support for Avro container readers by tracking block boundaries (offset + record count) during iteration and exposing an API to seek back to a previously-seen block for Read + Seek inputs.

Changes:

  • Introduced BlockPosition and internal position tracking to record block start offsets as blocks are loaded.
  • Added Reader::{current_block,data_start,seek_to_block} (seek API gated on Seek) plus tests validating seeking between blocks.
  • Added a new error detail variant for seek failures.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
avro/src/reader/mod.rs Exposes BlockPosition and adds reader-level block position + seek APIs with new tests.
avro/src/reader/block.rs Implements BlockPosition, PositionTracker, and block-level seek + block-boundary tracking.
avro/src/lib.rs Re-exports BlockPosition from the crate root.
avro/src/error.rs Adds Details::SeekToBlock for I/O seek errors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread avro/src/reader/block.rs Outdated
Comment thread avro/src/reader/mod.rs
Comment thread avro/src/reader/mod.rs Outdated
Comment thread avro/src/reader/block.rs Outdated
@martin-g
Copy link
Copy Markdown
Member

martin-g commented Apr 6, 2026

Added a feature to seek to a particular block when reading an Avro file.

Do you have a use case for this functionality ?

Comment thread avro/src/reader/block.rs
let n = self.inner.read(buf)?;
self.pos += n as u64;
Ok(n)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation assumes that read_buf(), read_exact(), read_vectored(), ... use their default impls and delegate to read(). But if they are overwritten then the tracking breaks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any common examples for this behavior?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tracking is done by wrapping the reader in a PositionTracker right? So it will always go through PositionTracker::read.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem I see is:

  • imagine some custom Read implementation that overrides any of the provided methods (all Read methods but read()) for any reason. For example let's say the custom Read impl has a custom version of read_exact()
  • PositionTracker is not opt-in and thus any usage of Read::read_exact() in the Avro codebase will always use PositionTracker::read_exact(). This will use the default impl (https://doc.rust-lang.org/stable/src/std/io/mod.rs.html#1044-1046) that will delegate to PositionTracker::read(). It will delegate to inner::read() and increment the read bytes (pos).
  • So, the custom Read impl read_exact() won't be used at all and the user application has nothing to do

PositionTracker either needs to be smarter or opt-in.

Comment thread avro/src/reader/mod.rs Outdated
Comment thread avro/src/reader/block.rs Outdated
Comment thread avro/src/reader/block.rs

self.current_block_info = Some(BlockPosition {
offset: block_start,
message_count: block_len as usize,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message_count: block_len as usize,
message_count: self.message_count,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wrote this part specifically this way to ensure it would not lost, if a refactoring or other change are applied. It cannot rely on the meaning of the self.message_count and its current value if those two will be separated into different places of code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can move self.message_count = block_len as usize down next to self.current_block_info to reduce that risk. As I do think Martin's suggestion is reasonable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not arguing, any of this would work and I can change, that's no problem. But I'm curios to understand why you think this would be better?

My idea is just using the source of truth variable, so there is no way it gets a wrong number. Using the self.message_count works with the current code, but it doesn't give any guarantee if it changes. So I'm wondering why the second approach is better?

@splix
Copy link
Copy Markdown
Contributor Author

splix commented Apr 6, 2026

Added a feature to seek to a particular block when reading an Avro file.

Do you have a use case for this functionality ?

I need to read just a few records from a large Avro file, and without this it's incredibly inefficient as I need to read the whole file from the start each time.

@splix
Copy link
Copy Markdown
Contributor Author

splix commented Apr 27, 2026

I'm wondering if you guys expect me to make some changes to the PR or we're waiting for something else?

@Kriskras99
Copy link
Copy Markdown
Contributor

I don't have further comments, @martin-g how about you?

@martin-g martin-g marked this pull request as draft April 29, 2026 05:09
Comment thread avro/src/reader/block.rs
let n = self.inner.read(buf)?;
self.pos += n as u64;
Ok(n)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem I see is:

  • imagine some custom Read implementation that overrides any of the provided methods (all Read methods but read()) for any reason. For example let's say the custom Read impl has a custom version of read_exact()
  • PositionTracker is not opt-in and thus any usage of Read::read_exact() in the Avro codebase will always use PositionTracker::read_exact(). This will use the default impl (https://doc.rust-lang.org/stable/src/std/io/mod.rs.html#1044-1046) that will delegate to PositionTracker::read(). It will delegate to inner::read() and increment the read bytes (pos).
  • So, the custom Read impl read_exact() won't be used at all and the user application has nothing to do

PositionTracker either needs to be smarter or opt-in.

Comment thread avro/src/reader/mod.rs
/// Typically the caller saves offsets from [`current_block`](Self::current_block)
/// during forward iteration and later passes them here to jump back.
pub fn seek_to_block(&mut self, offset: u64) -> AvroResult<()> {
let seek_status = self.block.seek_to_block(offset);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic has the side effect of resetting the errored flag.
If errored was set to true earlier, e.g. a call to Reader::next() then a call to Reader::seek_to_block(offset) that successfully reads the block at that offset will reset the errored to false. Now the user application can again call Reader::next() (or any other method that checks errored before doing any work).

Comment thread avro/src/reader/block.rs
pub(super) fn seek_to_block(&mut self, offset: u64) -> AvroResult<()> {
self.reader
.seek(SeekFrom::Start(offset))
.map_err(Details::SeekToBlock)?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be safer to reset the state first (lines 368-371).
Currently if SeekToBlock is returned then the state won't be reset and if Block::read_block_next() is used it will fail at

assert!(self.is_empty(), "Expected self to be empty!");

Comment thread avro/src/reader/block.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlockPosition {
/// Byte offset in the stream where this block starts (before the object-count varint).
pub offset: u64,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since BlockPosition is public I'd recommend to make the fields private, add constructor and getters. This way it will be easier to add more fields later if needed without API breaks.

Comment thread avro/src/reader/mod.rs
let mut reader = Reader::new(Cursor::new(&data))?;
let result = reader.seek_to_block(7);
assert!(result.is_err());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extend this test with seek to EOF and beyond:

Suggested change
let eof = data.len() as u64;
assert!(reader.seek_to_block(eof).is_err());
assert!(reader.seek_to_block(eof + 1).is_err());

Comment thread avro/src/reader/block.rs
// and replace `buf` with the new one, instead of reusing the same buffer.
// We can address this by using some "limited read" type to decode directly
// into the buffer. But this is fine, for now.
self.codec.decompress(&mut self.buf)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.current_block_info will be set even if the decompression fails.
And Reader::current_block() will see it.

@martin-g
Copy link
Copy Markdown
Member

Added a feature to seek to a particular block when reading an Avro file.

Do you have a use case for this functionality ?

I need to read just a few records from a large Avro file, and without this it's incredibly inefficient as I need to read the whole file from the start each time.

AFAIU you need to read the Avro data once to collect the offsets to be able to seek later, right ?
So, this helps for the following reads ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants