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

Implement sqlx::IntoArguments for Values #275

Merged
merged 1 commit into from
Jun 26, 2022

Conversation

nitnelave
Copy link
Contributor

Fixes #273

PR Info

Adds

Implements sqlx::IntoArguments for Values.
Implement IntoIterator for Values.

Breaking Changes

It will break for users who have enabled an optional feature (e.g. with-chrono) and an sqlx backend.
To avoid that, we could add yet another feature to guard the conversion, but it would make the API less discoverable. On the other hand, the errors you get in the scenario above are very clear (it tells you which feature you need to enable).

@nitnelave
Copy link
Contributor Author

Small question: when writing a u64 to Postgres or Sqlite, since they don't support that format, I try to convert it to a i64 and unwrap if it fails. Would it perhaps be better to fail-fast by panicking for all inputs, forcing the user to do the conversion themselves?
Similarly, for Postgres, I promote small unsigned to bigger signed integers (because PG doesn't support unsigned). That will always work, but might not be ideal? Not sure about that one, if we end up serializing that to a string it shouldn't matter. It would be a bit annoying on the user side to be forced to convert everything to signed just because, even though it works like that.

@nitnelave
Copy link
Contributor Author

Example usage in a real project: lldap/lldap#127

@Sytten
Copy link
Contributor

Sytten commented Mar 23, 2022

This is a big shift from the macro (https://github.com/SeaQL/sea-query/blob/master/src/driver/sqlx_sqlite.rs) that was used previously to generate that.

Like I suggested a while ago I think those integrations should be in different crates then you can greatly simplify the number of features and stay consistent between this and the bind macro.

Different crates would also allow me to write the diesel integration.

@nitnelave
Copy link
Contributor Author

One problem with a different crate is that you can't condition on the sea query features that are enabled (or can you? I didn't find out how to do that for sqlx here). That means that you'll have to repeat the same feature flags for the other crate.

Here we can remove all the feature flags that I added with their sqlx versions, you'll just get a not very clear error message if you enable a feature for sea query but not sqlx.

How would the conversion look like in the other crate? Adding a .to_sqlx method to Values, via a trait?

Anyway, we should at least keep the IntoIterator part of this PR, it can even be used in the existing macros to avoid some copies.

@Sytten
Copy link
Contributor

Sytten commented Mar 24, 2022

If we do a separate crate I think its fair to have duplicate features and that a feature X can also enable it on sea-query (like you already did) so you dont have to specify it twice.

I mean yeah that could work too. I am just trying to think on what is more confusing for the user. I think that if you enable sqlx-mysql and with-chrono on sea-query its fair to assume that you need to enable chrono on the sqlx dependency too. Documentation can help on that.

Hum yeah this is more annoying and one major point in favor of not splitting in another crate. Maybe then its a matter of splitting it in modules so they are not all in one giant file and its easy to enable/disable each flavour. I do plan on bringing my diesel implementation in the same format so lets try to make that work.

@tyt2y3
Copy link
Member

tyt2y3 commented Mar 24, 2022

Welcome back, long time no see!

Anyway, we should at least keep the IntoIterator part of this PR, it can even be used in the existing macros to avoid some copies.

Agree.

The reason I don't want to include sqlx as a dependency is because yeah, it forces us to select a runtime and that's not where sea-query should do. Ideally, sqlx (like postgres) would split out a types crate so that we can only depend on the pure type definitions.

I also can't think of a better idea than moving this IntoArguments feature to another sub-crate.

@nitnelave
Copy link
Contributor Author

nitnelave commented Mar 24, 2022

The reason I don't want to include sqlx as a dependency is because yeah, it forces us to select a runtime

Only for tests. Otherwise, the only requirement is that in the final binary, a runtime is selected. It's still up to the user to choose what runtime they want. If we have a non-default feature like I added there, it's not a problem (and I only added it to make --all-features work, otherwise it's really not needed).

Yeah, ideally sqlx would split out the core types, but that's non-trivial work.

Moving IntoArguments into a sub-crate makes the interface less friendly, in my opinion.

@nitnelave
Copy link
Contributor Author

Another advantage of IntoArguments vs the macros is that it supports dynamic DB backend, the Any type of sqlx. It delays the conversion until sqlx calls it, at which point they know what backend is actually used.

@nitnelave
Copy link
Contributor Author

Hmm, no, actually this won't help with the argument resolution, we'd still have to implement it for Any as well.

Anyway, what's the decision on this? Do we want to cut this back to only IntoIterator, or keep it like this?

@Sytten
Copy link
Contributor

Sytten commented Apr 3, 2022

I think this needs to wait on the merge of #292

@ikrivosheev
Copy link
Member

@Sytten I agree. @nitnelave If you need help adapting your PR, I'm ready to help.

@nitnelave
Copy link
Contributor Author

Thanks for the offer @ikrivosheev ! I should be able to handle it myself, once I understand what I'm adapting to :) What do we want to implement? Are we still on board for the intoargument trait?

@nitnelave
Copy link
Contributor Author

I see that #292 is merged. AFAICT, it's just a refactoring of the drivers, so I'm not sure it changes much our approach here.
What do we want to do for this? Do we want to implement IntoArguments? I'd love to have IntoArguments for Any as well.

@Sytten
Copy link
Contributor

Sytten commented Apr 14, 2022

With rust 1.60 we can now do crate?/feature which is ideal for the use case we have here. Though requiring 1.60 would be a bit brutal. @tyt2y3 @billy1624

@nitnelave
Copy link
Contributor Author

@tyt2y3 any opinion here? Do we want to proceed with this PR or not? If we do, I'll add support for the Any database as well.

@ikrivosheev
Copy link
Member

@nitnelave I like idea @tyt2y3 about: don't bring sqlx into sea-query. Two variants:

  1. move into other subcrate
  2. move into drivers

But I don't understand what is different between this and current driver implants?

@nitnelave
Copy link
Contributor Author

The problem is that we can't implement a trait on Values in another crate. The only possibility would be that we somehow extract the entire Values struct (and dependencies) to another crate, and implement the trait there, but that's still ugly (sea-query depends on that type definition, which depends on sqlx for the trait).

Note that the sqlx dependency is there only if you enable the sqlx feature.

The reason why I consider this approach superior to drivers is:

  • The syntax is nicer, it's just "normal" sqlx syntax, with no macro involved.
  • It works with Sqlx's expected types and traits directly, instead of working around them with multiple calls to bind
  • It works with generics: if the function called works on a Pool<DB>, then implementing the trait will make sure that the correct DB is picked, whereas with macros you have to specify directly which DB you're using.

@nitnelave
Copy link
Contributor Author

One idea I can come up with is to have a sub crate that implements a new trait on the various queries, adding a .build_sqlx method instead of build, that returns a wrapper type around Values with IntoArguments implemented.

It's kinda hard to discover though, in that case we should probably add some comments here around the sqlx feature referencing that crate.

@Sytten
Copy link
Contributor

Sytten commented Apr 18, 2022

I would be in favour of moving values to the driver crate, maybe it could be renamed to better express that.

@nitnelave
Copy link
Contributor Author

That doesn't really solve the issue of depending on sqlx: sea-query depends on the driver crate, which depends on sqlx. Plus you'll have to move all the dependencies of Value to that crate as well. I don't think it would bring much clarity.

@ikrivosheev
Copy link
Member

@nitnelave, no, sea-query-driver doesn't depend on sqlx...

@ikrivosheev
Copy link
Member

But I like your idea and the interface is convenient.

@Sytten
Copy link
Contributor

Sytten commented Apr 18, 2022

Agreed but it is a solved problem on 1.60 though as you can mix optional dependencies with features so its just a matter of waiting a couple of weeks for people to upgrade IMO.

@nitnelave
Copy link
Contributor Author

@nitnelave, no, sea-query-driver doesn't depend on sqlx...

Well, not now, but if we implement IntoArguments for Values in sea-query-driver, then it will (to the same extent that we were talking about sea-query depending on sqlx). The dependency doesn't really change, it's just hidden in a subcrate that we depend on.

@billy1624
Copy link
Member

We could implement sqlx::IntoArguments inside sea-query-driver with macros, which means sea-query-driver doesn't depends on sqlx. The macros will define its own Values which wrap the sea_query::Values.

pub struct Values(pub sea_query::Values);

Then, IntoIterator and sqlx::IntoArguments can be implemented as follows

impl IntoIterator for Values {
    type Item = sea_query::Value;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl<'q> sqlx::IntoArguments<'q, sqlx::postgres::Postgres> for Values {
    ...
}

@nitnelave
Copy link
Contributor Author

@billy1624 but you still need the macros to wrap the values. Can we merge this with the idea I had above, implementing a new method (via a trait) on the queries that pre-wraps the values?

@tyt2y3
Copy link
Member

tyt2y3 commented Apr 21, 2022

@billy1624 but you still need the macros to wrap the values. Can we merge this with the idea I had above, implementing a new method (via a trait) on the queries that pre-wraps the values?

Oh, so it's like the current PR, but in an external crate?

We could implement sqlx::IntoArguments inside sea-query-driver with macros, which means sea-query-driver doesn't depends on sqlx. The macros will define its own Values which wrap the sea_query::Values.

I guess this is the same as what we are doing with Rusqlite now.

@nitnelave
Copy link
Contributor Author

@tyt2y3 it would be a mix of my external crate proposal and @billy1624 's proposal: the driver macros introduce a new wrapping Values type and implement IntoArgument for it. And in addition, they create a new trait and implement it for all queries, that adds a .build-sqlx method which calls .build and wraps the returned Values in the new type. That way from outside you just have to use the new trait, call build_sqlx and everything works fine.

@tyt2y3
Copy link
Member

tyt2y3 commented Apr 22, 2022

build_sqlx and everything works fine

Sounds like an intricate plan. But that's our end goal yeah. As long as 1) it does not affect existing users 2) is opt-in we will merge it and see how it works in action, and might actually replace our current driving mechanism (which is a bit hacky IMO)

@nitnelave
Copy link
Contributor Author

I updated the implementation, it should be rather complete now. I also added an example for using with any, the flow is pretty nice (but the data limitations are pretty tough, you're pretty much limited to the very basic types).

@Sytten I'm not sure the two drivers will have anything in common: in the case of sqlx we need to implement IntoArguments which is sqlx-specific, and same goes for diesel I expect. We could fit all under a same type, but I'm not sure why we would do that.

At most I can picture a shared wrapper, something like BindValues but the rest is very implementation-dependent.

@nitnelave
Copy link
Contributor Author

nitnelave commented Apr 25, 2022

One difference between this and the driver approach (more of a difference of choice): the drivers "support" the time arguments for sqlite by calling time_as_naive_utc_in_string. But that's going to fail on the way back, decoding the time argument. If the support is not built into sqlx, I'm of the opinion that it's up to the user to make a well-behaved wrapper or find an alternative.

EDIT: in the example, it's asymmetric: there is custom code to decode, but not to encode. With my update, it makes it more consistent by making the encoding appear.

@Sytten
Copy link
Contributor

Sytten commented Apr 25, 2022

Agreed that is what I was suggesting, a BindValues and modifying the select trait implementation so its always the same function name to convert to this type.

@nitnelave
Copy link
Contributor Author

Any suggestion for the function name? build_to_bind? build_bind_values?

@Sytten
Copy link
Contributor

Sytten commented Apr 25, 2022

Hum no particular idea, just throwing a couple more in the hat: build_binding, build_bind, build_binder.
Heck even just bind could be good maybe?

@nitnelave
Copy link
Contributor Author

Hmm, I disagree with bind, it doesn't bind at all, it just builds. The values produced can be bound easily, but this doesn't bind.

@Sytten
Copy link
Contributor

Sytten commented Apr 25, 2022

Them a build_bindable returning a BindableValues could be more accurate, but I really dont have a strong opinion on it

@nitnelave
Copy link
Contributor Author

@billy1624 @tyt2y3 any opinion on the Values/BindValues/build_bindable issue? Or the PR as a whole?

Copy link
Member

@billy1624 billy1624 left a comment

Choose a reason for hiding this comment

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

Thanks! @nitnelave

args.add(i.map(Into::<i64>::into));
}
Value::BigUnsigned(i) => {
args.add(i.map(|i| <i64 as std::convert::TryFrom<u64>>::try_from(i).unwrap()));
Copy link
Member

Choose a reason for hiding this comment

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

This might result in unexpected panic
I guess conversion is required because PostgreSQL doesn't support u64?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. How else would you handle this? Any is limited to the subset that every DB supports.

Comment on lines 70 to 72
Value::ChronoDateTimeWithTimeZone(_) => {
panic!("MySql doesn't support fixed offset chrono types");
}
Copy link
Member

Choose a reason for hiding this comment

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

We can bind it as string just like what we did at

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines +34 to +36
Value::BigUnsigned(i) => {
args.add(i.map(|i| <i64 as std::convert::TryFrom<u64>>::try_from(i).unwrap()));
}
Copy link
Member

Choose a reason for hiding this comment

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

This might result in unexpected panic

Same here.

Btw... I think the related implementation inside sea-query-driver is faulty as well. We just case u64 as i64. It will (integer) underflow silently.

CC @tyt2y3 @ikrivosheev

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no correct behavior here, unless you just want to panic on every u64 (and same for Any).

WDYT?

sea-query-binders/src/sqlx_sqlite.rs Outdated Show resolved Hide resolved
examples/sqlx_any/Cargo.toml Outdated Show resolved Hide resolved
Copy link
Member

@ikrivosheev ikrivosheev left a comment

Choose a reason for hiding this comment

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

@nitnelave thank you for contribution! Some comments

sea-query-binders/Cargo.toml Show resolved Hide resolved
@nitnelave
Copy link
Contributor Author

@ikrivosheev @billy1624 Any other comments?

Copy link
Member

@ikrivosheev ikrivosheev left a comment

Choose a reason for hiding this comment

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

@nitnelave, thank you! Sory for delay. LGTM!

@nitnelave
Copy link
Contributor Author

Thanks for the review! Can we merge this, then?

@billy1624 billy1624 requested a review from tyt2y3 May 23, 2022 07:24
@nitnelave nitnelave force-pushed the sqlx_intoarguments branch from 85db1ab to 7d27267 Compare May 23, 2022 08:57
@nitnelave nitnelave force-pushed the sqlx_intoarguments branch from 7d27267 to 445b660 Compare May 23, 2022 09:08
@nitnelave
Copy link
Contributor Author

@tyt2y3 when you have time for a review :)

@nitnelave
Copy link
Contributor Author

Any chance of a review?

@tyt2y3 tyt2y3 changed the base branch from master to sea-query-binder June 26, 2022 06:02
@tyt2y3 tyt2y3 merged commit 938fe29 into SeaQL:sea-query-binder Jun 26, 2022
@tyt2y3
Copy link
Member

tyt2y3 commented Jun 26, 2022

I am about to make some changes and merge into master

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.

Implement sqlx::IntoArguments for Values
5 participants