Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated columns -- handling differences between database inputs and outputs #24

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

ulucs
Copy link
Contributor

@ulucs ulucs commented Apr 25, 2024

This pull request currently implements a #[sql(generated)] attribute, so that the query generator can skip inserting columns that would be generated by the database, such as auto-generated or calculated columns.

However, since we are using Rust structs, the initial struct would also have to be initialized with dummy values and I believe leveraging the type system to differentiate between the dummy values and those returned by the database will be very useful. I can currently think of two ways of achieving this, first: would it make sense to separate the types of the data we are putting into the database and the data that is returned by the database?

An example code would be like this, assuming the id field is auto-generated by the database:

    let userInput = UserInput { name: "demo".to_owned(), email: "[email protected]".to_owned() };

    let user = userInput.save(&pool).await?; // now it has the type User, and the id field
    //// or, an alternative way of inserting into the database
    //// let user = User::create(&userInput, &pool).await?;
    let postInput = PostInput { author: user.id, title: "test1".to_owned() };

    postInput.save(&pool).await?

The pros that come to my mind are:

  1. We have different structs for different data shapes, letting us know exactly what we are dealing with
  2. Verification logic has a very natural place to live, and we know that any User struct will only have data that the database has verified
  3. Patching comes free design-wise, which would save a round-trip as opposed to updating and is atomic without needing transactions

On the other hand, it is a design change (ie expensive) and how to create these structs is another question to be solved.

Another option is one I am lifting from how Ecto handles eager loading (ie joining relationships): any field that is to be generated has no value when initialized (Rust-side) and obtains a value after connecting to the database, ie an Option. But, since the interaction with database already returns a Result, using Result<value, Uninitialized> as an Option will prevent the nested Result->Option (and what is an Option anyway, but a Result with a single Error mode):

    //// somewhere in Atmosphere
    let Uninitialized = Err("uninitialized value");
    type Generated<T> = Result<T, Uninitialized>;


    //// while using Atmosphere
    let user = User { id: Uninitialized, name: "demo".to_owned(), email: "[email protected]".to_owned() };

    let user = user.save(&pool).await?; // utilizing shadowing to overwrite user, else user would have to become mutable
    let post = Post { author: user.id?, title: "test1".to_owned() };

    post.save(&pool).await?

The pros of this are:

  1. Required changes are much smaller compared to the previous option
  2. User-facing API is almost the same, just requiring a ? for accessing database-generated fields

However, this method does not encode whether we have access to the generated value in the type system; so we will always have to go through the Result type to access the value. In contrast, the other method will never lead to the user reaching an Uninitialized value, while the compiler does not prevent the user from accidentally trying to access an Uninitialized in this case.

So, I am quite interested in which way you think I should go forward or if there are different ways of implementing the result that did not come to my mind. And I apologize for any Rust errors in the examples :D

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