I someone would ask me if I know Rust, I’d tell them that I know my way around, I also feel that I have enough knowledge to put myself into intermediate-advanced seat when it comes to the language and its behaviour. Yet, I hadn’t had opportunity to work with very complex trait bounds and thanks to Diesel it was all about to change.
In short Diesel is a ORM/Query Builder for Rust. I migrated recently to it from SQLX because I really disliked it’s ergonomics. Not only I had issues with live precompilation (especially since one of my library crates is setting up schema during application bootup) but I was disappointed in runtime failures. The straw that broke camel’s back, however, was absolute impossibility of reusing queries. Types were just too crazy to mash two pieces together and ultimately I gave up.
I’m quite happy with the choice, but recently I wanted to reuse some bits, and it’s a WILD RIDE. I’m still happy about Diesel, but it just wasn’t that easy.
Look at the following example:
type Query = ?;
type Result = ?;
fn order_query(query: Query, params: &Params) -> ?
{
let asc = matches!(params.order, Some(SortOrder::Asc));
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
I have 3 very similar (outside of number of tables in joins and data selected) queries that I want to be sorted. Sorting has to be added externally because I plan to have multiple sorting parameters. Right now, I have only two. Having ordering inlined in 3+ functions isn’t something that I’d be happy with.
Above code won’t compile.
First problem that has to be solved - what’s the input and the output of the function. The answer is of my favorite kinds: it depends.
Diesel is typing queries very hard which means that input type will change for each query type. One possibility would be implementing for each type but that defeats the purpose. Lazy person idea is - what if I just used generics?
type Query = ?;
fn order_query<Q,R>(query: Q, params: &Params) -> R
{
let asc = matches!(params.order, Some(SortOrder::Asc));
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
There are problems and this won’t compile because:
no method named then_order_by found for type parameter Q in the current scope
items from traits can only be used if the type parameter is bounded by the trait
then_order_by is implemented by ThenOrderByDsl so I’m going to constraint my Q with that:
fn order_query<Q, R>(query: Q, params: &ListingsParams) -> R
where
Q: ThenOrderDsl<_>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
Another problem! ThenOrderDsl takes one generic that needs to implement Expression trait
fn order_query<Q, R, E>(query: Q, params: &ListingsParams) -> R
where
E: Expression,
Q: ThenOrderDsl<E>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
Once again this won’t compile:
mismatched types
expected type parameter E
found struct diesel::expression::operators::Asc<schema::listings::columns::created_at>
---
mismatched types
expected type parameter `E`
found struct `diesel::expression::operators::Asc<schema::listings::columns::title>`
So we have problem with E - isn’t really one type, it’s 4 types in the current query. So maybe drop E and constrain Q instead:
fn order_query<Q, R>(query: Q, params: &ListingsParams) -> R
where
Q: ThenOrderDsl<Asc<listings::created_at>>
+ ThenOrderDsl<Desc<listings::created_at>>
+ ThenOrderDsl<Asc<listings::title>>
+ ThenOrderDsl<Desc<listings::title>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
mismatched types
expected type parameter `R`
found associated type `<Q as ThenOrderDsl<diesel::expression::operators::Asc<schema::listings::columns::created_at>>>::Output`
the caller chooses a type for `R` which can be different from `<Q as ThenOrderDsl<Asc<created_at>>>::Output`
I’ll change R to associated type like this:
fn order_query<Q>(
query: Q,
params: &ListingsParams,
) -> <Q as ThenOrderDsl<Asc<listings::created_at>>>::Output
where
Q: ThenOrderDsl<Asc<listings::created_at>>
+ ThenOrderDsl<Desc<listings::created_at>>
+ ThenOrderDsl<Asc<listings::title>>
+ ThenOrderDsl<Desc<listings::title>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
and I get this error:
match arms have incompatible types
expected associated type <Q as ThenOrderDsl<diesel::expression::operators::Asc<schema::listings::columns::created_at>>>::Output
found associated type <Q as ThenOrderDsl<diesel::expression::operators::Desc<schema::listings::columns::created_at>>>::Output
It’s surprising until you know it. Diesel is typing very hard, so obviously different queries will yield different types. Good to know, but what can be done?
The answer is boxed query. It’s possible to type erase query which will allow us to make any kind of transformation. We cannot expect caller to know about that though, so we’ll box inside:
fn order_query<Q, R>(
query: Q,
params: &ListingsParams,
) -> <Q as ThenOrderDsl<Asc<listings::created_at>>>::Output
where
Q: ThenOrderDsl<Asc<listings::created_at>>
+ ThenOrderDsl<Desc<listings::created_at>>
+ ThenOrderDsl<Asc<listings::title>>
+ ThenOrderDsl<Desc<listings::title>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
let query = query.into_boxed();
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
New error:
no method named into_boxed found for type parameter Q in the current scope
items from traits can only be used if the type parameter is bounded by the trait (rustc E0599)
hint: method `into_boxed` not found for this type parameter
Of course it can’t be boxed, because we said that query can use then_order_by we didn’t mention anything about boxing. Let’s make it so:
fn order_query<'a, Q, R>(
query: Q,
params: &ListingsParams,
) -> <Q as ThenOrderDsl<Asc<listings::created_at>>>::Output
where
Q: BoxedDsl<'a, DB>
+ QueryDsl
+ ThenOrderDsl<Asc<listings::created_at>>
+ ThenOrderDsl<Desc<listings::created_at>>
+ ThenOrderDsl<Asc<listings::title>>
+ ThenOrderDsl<Desc<listings::title>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
let query = query.into_boxed();
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
It boxed alright, but now we have new problem:
the method then_order_by exists for associated type <Q as BoxedDsl<'_, Pg>>::Output, but its trait bounds were not satisfied
the following trait bounds were not satisfied:
<Q as BoxedDsl<'_, Pg>>::Output: diesel::Table
which is required by <Q as BoxedDsl<'_, Pg>>::Output: ThenOrderDsl<_>
<Q as BoxedDsl<'_, Pg>>::Output: diesel::query_builder::Query
which is required by <Q as BoxedDsl<'_, Pg>>::Output: ThenOrderDsl<_>
<Q as BoxedDsl<'_, Pg>>::Output: ThenOrderDsl<_>
which is required by <Q as BoxedDsl<'_, Pg>>::Output: ThenOrderDsl<_>
[CUT]
method is there, but we hadn’t constrained in correctly. into_boxed() is provided by QueryDsl but then we need to make sure out query can be Boxed so BoxDsl<'a, DB> - where DB is out current postgresql and 'a is required lifetime. I also moved back to R as result.
fn order_query2<'a, Q, R>(query: Q, params: &ListingsParams) -> R
where
Q: BoxedDsl<'a, DB> + QueryDsl,
IntoBoxed<'a, Q, DB>: ThenOrderDsl<Asc<listings::created_at>>
+ ThenOrderDsl<Desc<listings::created_at>>
+ ThenOrderDsl<Asc<listings::title>>
+ ThenOrderDsl<Desc<listings::title>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
let query = query.into_boxed();
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
While this is fixed, the old error came back to haunt us:
mismatched types
expected type parameter R
found associated type <<Q as BoxedDsl<'_, Pg>>::Output as ThenOrderDsl<diesel::expression::operators::Asc<schema::listings::columns::created_at>>>::Output
you might be missing a type parameter or trait bound
the caller chooses a type for R which can be different from <<Q as BoxedDsl<'_, Pg>>::Output as ThenOrderDsl<Asc<...>>>::Output
I just promised that boxing will solve the problem. Yes it can! But we need to constrain types harder, especially to notify Rust that our types on each arms are going to be the same!
fn order_query<'a, Q>(query: Q, params: &ListingsParams) -> IntoBoxed<'a, Q, DB>
where
Q: BoxedDsl<'a, DB> + QueryDsl,
IntoBoxed<'a, Q, DB>: ThenOrderDsl<Asc<listings::created_at>, Output = IntoBoxed<'a, Q, DB>>
+ ThenOrderDsl<Desc<listings::created_at>, Output = IntoBoxed<'a, Q, DB>>
+ ThenOrderDsl<Asc<listings::title>, Output = IntoBoxed<'a, Q, DB>>
+ ThenOrderDsl<Desc<listings::title>, Output = IntoBoxed<'a, Q, DB>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
let query = query.into_boxed();
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
Now it works. I’ll dissect it into pieces (because it took me some time to decypher myself (: )
fn order_query<'a, Q>(query: Q, params: &ListingsParams) -> IntoBoxed<'a, Q, DB>
where
// My Q should be boxable (BoxedDsl) and have `into_boxed()` (QueryDsl)
Q: BoxedDsl<'a, DB> + QueryDsl,
// My boxed Q...
IntoBoxed<'a, Q, DB>:
// ...should be then_order_by ASC by listings::created_at and returned boxed query (IntoBoxed<_,Q,_>)
ThenOrderDsl<Asc<listings::created_at>, Output = IntoBoxed<'a, Q, DB>>
// ...and should be then_order_by DESC by listings::created_at and returned boxed query (IntoBoxed<_,Q,_>)
+ ThenOrderDsl<Desc<listings::created_at>, Output = IntoBoxed<'a, Q, DB>>
// and ...should be then_order_by ASC by listings::title and returned boxed query (IntoBoxed<_,Q,_>)
+ ThenOrderDsl<Asc<listings::title>, Output = IntoBoxed<'a, Q, DB>>
// and ...should be then_order_by DESC by listings::title and returned boxed query (IntoBoxed<_,Q,_>)
+ ThenOrderDsl<Desc<listings::title>, Output = IntoBoxed<'a, Q, DB>>,
{
let asc = matches!(params.order, Some(SortOrder::Asc));
let query = query.into_boxed();
match (params.sort.clone(), asc) {
(Some(SortField::Date), true) => query.then_order_by(listings::created_at.asc()),
(Some(SortField::Date), false) => query.then_order_by(listings::created_at.desc()),
(Some(SortField::Title), true) => query.then_order_by(listings::title.asc()),
(Some(SortField::Title), false) => query.then_order_by(listings::title.desc()),
(None, _) => query.then_order_by(listings::created_at.desc()),
}
}
and back to signature:
// order_query should take a (constrained) Q and return boxed query (IntoBoxed<_,Q,_>)
fn order_query<'a, Q, R>(query: Q, params: &ListingsParams) -> IntoBoxed<'a, Q, DB>
{
// ...
}
My two reflections are:
- It’s fun to do complex trait bounds (in Diesel, maybe elsewhere too!)
- It’s neat how Dieseli allows query composition while keeping rigid grip on types
Przemysław Alexander Kamiński
vel xlii vel exlee
Powered by hugo and hugo-theme-nostyleplease.