Skip to content

Conversation

rtyler
Copy link
Member

@rtyler rtyler commented Aug 31, 2025

The SnapshotBuilder is a reasonable improvement IMHO, but the fluent
style because challenging when the caller has an Option<u64> as well.

I have been fond of the AWS SDK fluent
builders

which provide a account_id(id) and set_account_id(Option<id>) style
builders which give callers flexibility.

Instead of:

let mut builder = Snapshot::builder(url);
builder  = match version.as_ref() {
  Some(version) => builder.at_version(version),
  None => builder,
};
let snapshot = builder.build(engine.as_ref();

Callers can instead:

let snapshot =
Snapshot::builder(url).set_version(version).build(engine.as_ref());

The `SnapshotBuilder` is a reasonable improvement IMHO, but the fluent
style because challenging when the caller has an `Option<u64>` as well.

I have been fond of the [AWS SDK fluent
builders](https://docs.rs/aws-credential-types/1.2.6/aws_credential_types/struct.CredentialsBuilder.html#method.set_account_id)
which provide a `account_id(id)` and `set_account_id(Option<id>)` style
builders which give callers flexibility.

Instead of:

```rust
let mut builder = Snapshot::builder(url);
builder  = match version.as_ref() {
  Some(version) => builder.at_version(version),
  None => builder,
};
let snapshot = builder.build(engine.as_ref();
```

Callers can instead:

```rust
let snapshot =
Snapshot::builder(url).set_version(version).build(engine.as_ref());
```

Signed-off-by: R. Tyler Croy <[email protected]>
Copy link

codecov bot commented Aug 31, 2025

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.63%. Comparing base (447767e) to head (5ea3f0a).

Files with missing lines Patch % Lines
kernel/src/snapshot/builder.rs 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1236      +/-   ##
==========================================
- Coverage   83.65%   83.63%   -0.03%     
==========================================
  Files         106      106              
  Lines       24966    24970       +4     
  Branches    24966    24970       +4     
==========================================
- Hits        20886    20883       -3     
- Misses       3046     3051       +5     
- Partials     1034     1036       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rtyler rtyler marked this pull request as ready for review August 31, 2025 18:54
@rtyler
Copy link
Member Author

rtyler commented Aug 31, 2025

@roeap FYI I hit this papercut when trying to integrate the latest main against delta-rs 😞

@roeap
Copy link
Collaborator

roeap commented Sep 1, 2025

@rtyler - i think for primitive types there is also the option to do

set_version(&self, version: impl Into<Option<Version>>) ...

which allows to pass either an option or the value.

What I wanted to look into - though not relevant for this specific case - if there is a way to get this also working when the inner type is something like Into<String>.

Comment on lines +46 to +50
/// Set the `version` [Option] explicitly.
pub fn set_version(mut self, version: Option<Version>) -> Self {
self.version = version;
self
}
Copy link
Member

Choose a reason for hiding this comment

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

What about at_version_opt (seems to match existing API better)? Or perhaps just unifying it all with at_version to take impl Into<Option> like @roeap says?

Copy link
Member

Choose a reason for hiding this comment

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

we had a decent bit of discussion on naming already - i'll let others chime in cc @nicklan @scovich @roeap

Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO the real problem is letting people set the version at all, as if that were somehow orthogonal to all the other information that could be passed (e.g. set version to 10 but pass P&M for latest version 15). Which points to having a builder with two different constructors -- one for a latest-version builder and one for an as-of-version builder. But that doesn't actually prevent the caller from passing latest P&M to a time travel builder, so meh.

Meanwhile, I'm totally fine with impl Into<Option>, we've used that trick elsewhere -- notably when setting start and end versions for CDF -- for the exact same papercut-avoidance that @rtyler aims for here.

We can adjust the API further if/when we decide to fix the larger ergonomics/footgun issue (which first requires having some idea of how to fix it, still TBD).

Copy link
Member

Choose a reason for hiding this comment

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

yea and to add the current proposal for the "different kind of versions": we will have a builder which bifurcates into two different types based on if you do an at_version(version) or with_metadata(P, M, version). see #1189 (comment)

@scovich
Copy link
Collaborator

scovich commented Sep 2, 2025

What I wanted to look into - though not relevant for this specific case - if there is a way to get this also working when the inner type is something like Into<String>.

I think you can do arg: impl Into<Option<impl Into<String>> -- the recipient has to do arg.into().map(Into::into). But from what I remember it can also be a pain in the neck for type inference?

@roeap
Copy link
Collaborator

roeap commented Sep 2, 2025

I think you can do arg: impl Into<Option<impl Into> -- the recipient has to do arg.into().map(Into::into). But from what I remember it can also be a pain in the neck for type inference?

this sometimes works:

struct MyBuilder {
      field1: Option<String>,
}

impl MyBuilder {
    fn with_field1<T: Into<String>>(mut self, field1: impl Into<Option<T>>) -> Self {
        self.field1 = field1.into().map(Into::into);
        self
    }
  }

But as you mention causes issues at the call site, even this does not work for me then ..

fn call1(arg: Option<String>) {
    let mut builder = MyBuilder { field1: None };
    let builder = builder.with_field1(arg);
}

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