Skip to content

feat(sdk): StreamReadOps trait and test-util fixture constructors - #659

Open
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1784752021-sdk-testability
Open

feat(sdk): StreamReadOps trait and test-util fixture constructors#659
devin-ai-integration[bot] wants to merge 4 commits into
mainfrom
devin/1784752021-sdk-testability

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

To unit-test its read/tailing logic, the connector in feldera/feldera#5728 had to define its own S2StreamClient trait and shadow copies of SDK response types (S2Batch/S2Record/S2Position) — because S2Stream has no trait to mock behind, and ReadBatch/SequencedRecord/StreamPosition aren't constructible in downstream tests. This ships both halves (a mockable trait is useless without constructible return types):

  • StreamReadOps — public, dyn-safe (#[async_trait]) trait covering the stream read ops (read, read_session, check_tail), implemented for S2Stream (named to leave room for a future StreamAppendOps):
    #[async_trait]
    pub trait StreamReadOps: Send + Sync {
        async fn read(&self, input: ReadInput) -> Result<ReadBatch, S2Error>;
        async fn read_session(&self, input: ReadInput) -> Result<Streaming<ReadBatch>, S2Error>;
        async fn check_tail(&self) -> Result<StreamPosition, S2Error>;
    }
    read_session returns Streaming<ReadBatch> rather than the concrete ReadSession so mocks can return any boxed stream; the S2Stream impl just boxes its ReadSession (whose Stream impl already yields Result<ReadBatch, S2Error>).
  • test-util feature — a documented public feature (distinct from the internal _hidden) gating plain-data fixture constructors: StreamPosition::new(seq_num, timestamp), SequencedRecord::from_parts(..) (gate widened from _hidden), and ReadBatch::new(records, tail).

sdk/tests/test_util.rs demonstrates the downstream pattern end-to-end: a Box<dyn StreamReadOps> mock yielding constructed batches, with no shadow types.

Testing

just fmt, just clippy (-D warnings), just test (679 passed; includes the feature-gated test since the repo test command uses --all-features), plus cargo clippy/test -p s2-sdk --features test-util.

Part of a set of SDK improvements from reviewing feldera/feldera#5728 (see #653, #654, #655, #658).

Link to Devin session: https://app.devin.ai/sessions/6a1c8bd8dcd144128571f1cab5af0dcb
Requested by: @shikhar

devin-ai-integration Bot and others added 2 commits July 22, 2026 20:28
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
@shikhar shikhar self-assigned this Jul 22, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two things the feldera connector needed to test against the SDK without shadow-copying SDK types: a ReadStreamOps trait that mocks can implement, and test-util-gated constructors for StreamPosition, SequencedRecord, and ReadBatch (all #[non_exhaustive]).

  • ReadStreamOps trait (sdk/src/ops.rs, sdk/src/lib.rs): #[async_trait]-decorated public trait with check_tail and read_session; implemented for S2Stream by boxing its concrete ReadSession into the Streaming<ReadBatch> alias. Re-exported from the crate root.
  • test-util feature (sdk/Cargo.toml, sdk/src/types.rs): Adds StreamPosition::new, ReadBatch::new, and widens SequencedRecord::from_parts from _hidden to also include test-util, giving downstream crates plain-data constructors under a stable, documented feature flag.
  • Integration test (sdk/tests/test_util.rs): Demonstrates the full downstream pattern — Box<dyn ReadStreamOps> backed by a MockReader that constructs fixtures via the new constructors.

Confidence Score: 4/5

Safe to merge; the changes are additive, well-tested, and do not touch any existing logic paths.

The trait and constructors are purely additive. The S2Stream impl correctly boxes ReadSession into Streaming<ReadBatch>, the test-util feature is cleanly separated from _hidden, and the integration test exercises the full mock pattern. The main open question is whether ReadStreamOps should carry Send + Sync supertraits — without them, callers in multi-threaded async code must write Box<dyn ReadStreamOps + Send + Sync> rather than the simpler Box<dyn ReadStreamOps>. The omission of the unary read method is noted as a potential completeness gap.

sdk/src/ops.rs — the ReadStreamOps trait definition, specifically supertrait bounds and method coverage.

Important Files Changed

Filename Overview
sdk/Cargo.toml Adds the public test-util feature flag, clearly documented and separate from the internal _hidden feature.
sdk/src/lib.rs Re-exports ReadStreamOps in the public API alongside the existing S2, S2Basin, S2Stream exports.
sdk/src/ops.rs Introduces ReadStreamOps trait with check_tail and read_session, and implements it for S2Stream. Missing Send + Sync supertraits limits ergonomics for callers using the trait object in async contexts, and the unary read method is absent from the interface.
sdk/src/types.rs Adds test-util-gated constructors for StreamPosition::new, SequencedRecord::from_parts (gate widened from _hidden), and ReadBatch::new. All three types are #[non_exhaustive], making these constructors necessary for downstream test fixtures.
sdk/tests/test_util.rs End-to-end integration test demonstrating the mocking pattern: Box<dyn ReadStreamOps> backed by MockReader using the new fixture constructors. Correctly guarded with #![cfg(feature = "test-util")].

Class Diagram

%%{init: {'theme': 'neutral'}}%%
classDiagram
    class ReadStreamOps {
        <<trait>>
        +check_tail() Result~StreamPosition, S2Error~
        +read_session(input ReadInput) Result~Streaming~ReadBatch~, S2Error~
    }

    class S2Stream {
        -client BasinClient
        -name StreamName
        -encryption Option~EncryptionKey~
        +check_tail() Result~StreamPosition, S2Error~
        +read(input ReadInput) Result~ReadBatch, S2Error~
        +read_session(input ReadInput) Result~ReadSession, S2Error~
        +append(input AppendInput) Result~AppendAck, S2Error~
    }

    class MockReader {
        <<example>>
        +check_tail() Result~StreamPosition, S2Error~
        +read_session(input ReadInput) Result~Streaming~ReadBatch~, S2Error~
    }

    class StreamPosition {
        +seq_num u64
        +timestamp u64
        +new(seq_num, timestamp)$ StreamPosition
    }

    class ReadBatch {
        +records Vec~SequencedRecord~
        +tail Option~StreamPosition~
        +new(records, tail)$ ReadBatch
    }

    class SequencedRecord {
        +seq_num u64
        +timestamp u64
        +body Bytes
        +headers Vec~Header~
        +from_parts(...)$ SequencedRecord
    }

    S2Stream ..|> ReadStreamOps : implements
    MockReader ..|> ReadStreamOps : implements
    ReadBatch "1" --> "0..*" SequencedRecord
    ReadBatch --> StreamPosition : tail
    ReadStreamOps ..> ReadBatch : yields
    ReadStreamOps ..> StreamPosition : returns
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
sdk/src/ops.rs:22-29
**Missing `Send + Sync` supertraits on `ReadStreamOps`**

The trait is designed for async usage, but `Box<dyn ReadStreamOps>` is `!Send` by default. Callers that store this in a struct used across `await` points or pass it to `tokio::spawn` must write `Box<dyn ReadStreamOps + Send + Sync>` everywhere. Adding `: Send + Sync` as supertraits would make `Box<dyn ReadStreamOps>` directly usable in multi-threaded tokio contexts — every existing implementor (`S2Stream`, the test `MockReader`) is already `Send + Sync`.

### Issue 2 of 2
sdk/src/ops.rs:22-29
**`read` omitted from `ReadStreamOps`**

`S2Stream` has a unary `read` method (single `ReadBatch`, not a streaming session) that is absent from `ReadStreamOps`. The trait name implies all read-side operations are covered, but a consumer needing to mock `read` alongside `check_tail`/`read_session` cannot do so through this interface. Is the omission intentional (scope limited to what feldera's connector uses), or should `read` be included for completeness?

Reviews (1): Last reviewed commit: "docs(sdk): document test fixture constru..." | Re-trigger Greptile

Comment thread sdk/src/ops.rs
Comment on lines +22 to +29
#[async_trait]
pub trait ReadStreamOps {
/// Check the current tail position.
async fn check_tail(&self) -> Result<StreamPosition, S2Error>;

/// Open a read session stream.
async fn read_session(&self, input: ReadInput) -> Result<Streaming<ReadBatch>, S2Error>;
}

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.

P2 Missing Send + Sync supertraits on ReadStreamOps

The trait is designed for async usage, but Box<dyn ReadStreamOps> is !Send by default. Callers that store this in a struct used across await points or pass it to tokio::spawn must write Box<dyn ReadStreamOps + Send + Sync> everywhere. Adding : Send + Sync as supertraits would make Box<dyn ReadStreamOps> directly usable in multi-threaded tokio contexts — every existing implementor (S2Stream, the test MockReader) is already Send + Sync.

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/src/ops.rs
Line: 22-29

Comment:
**Missing `Send + Sync` supertraits on `ReadStreamOps`**

The trait is designed for async usage, but `Box<dyn ReadStreamOps>` is `!Send` by default. Callers that store this in a struct used across `await` points or pass it to `tokio::spawn` must write `Box<dyn ReadStreamOps + Send + Sync>` everywhere. Adding `: Send + Sync` as supertraits would make `Box<dyn ReadStreamOps>` directly usable in multi-threaded tokio contexts — every existing implementor (`S2Stream`, the test `MockReader`) is already `Send + Sync`.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

Addressed: StreamReadOps now has Send + Sync supertraits.

Comment thread sdk/src/ops.rs
Comment on lines +22 to +29
#[async_trait]
pub trait ReadStreamOps {
/// Check the current tail position.
async fn check_tail(&self) -> Result<StreamPosition, S2Error>;

/// Open a read session stream.
async fn read_session(&self, input: ReadInput) -> Result<Streaming<ReadBatch>, S2Error>;
}

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.

P2 read omitted from ReadStreamOps

S2Stream has a unary read method (single ReadBatch, not a streaming session) that is absent from ReadStreamOps. The trait name implies all read-side operations are covered, but a consumer needing to mock read alongside check_tail/read_session cannot do so through this interface. Is the omission intentional (scope limited to what feldera's connector uses), or should read be included for completeness?

Was omitting read intentional (the connector only needs check_tail/read_session), or should read be included in ReadStreamOps for completeness?

Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/src/ops.rs
Line: 22-29

Comment:
**`read` omitted from `ReadStreamOps`**

`S2Stream` has a unary `read` method (single `ReadBatch`, not a streaming session) that is absent from `ReadStreamOps`. The trait name implies all read-side operations are covered, but a consumer needing to mock `read` alongside `check_tail`/`read_session` cannot do so through this interface. Is the omission intentional (scope limited to what feldera's connector uses), or should `read` be included for completeness?

 Was omitting `read` intentional (the connector only needs `check_tail`/`read_session`), or should `read` be included in `ReadStreamOps` for completeness?

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

Addressed: added the unary read to the trait for completeness (delegating impl on S2Stream, mock test extended).

@shikhar

shikhar commented Jul 22, 2026

Copy link
Copy Markdown
Member

Hmm, maybe we end up with StreamAppendOps and StreamReadOps as names go

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Renaming to StreamReadOps — will push shortly.

Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
@devin-ai-integration devin-ai-integration Bot changed the title feat(sdk): ReadStreamOps trait and test-util fixture constructors feat(sdk): StreamReadOps trait and test-util fixture constructors Jul 22, 2026
@shikhar

shikhar commented Jul 22, 2026

Copy link
Copy Markdown
Member

Devin, attend to Greptile's feedback

Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
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.

1 participant