Creating Diesel.rs queries with a dynamic number of .and()’s

The first thing you need to do is look at the complete error message:

error[E0308]: mismatched types
  --> src/main.rs:23:13
   |
23 |     inner = inner.and(author.like(format!("%{}%", authors[2])));//<2>
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `diesel::expression::operators::Like`, found struct `diesel::expression::operators::And`
   |
   = note: expected type `diesel::expression::operators::And<diesel::expression::operators::Like<_, _>, _>`
              found type `diesel::expression::operators::And<diesel::expression::operators::And<diesel::expression::operators::Like<_, _>, diesel::expression::operators::Like<schema::ebook::columns::author, diesel::expression::bound::Bound<diesel::sql_types::Text, std::string::String>>>, _>`

It’s long, but that’s because it’s fully qualified. Lets shorten the last part a bit:

expected type `And<Like<_, _>, _>`
   found type `And<And<Like<_, _>, Like<author, Bound<Text, String>>>, _>`

If you review the documentation for and, you’ll see that every call to and consumes the receiver and returns a brand new type — And:

fn and<T: AsExpression<Bool>>(self, other: T) -> And<Self, T::Expression>

This is the core of Diesel’s ability to generate strongly-typed SQL expressions with no run-time overhead. All of the work is delegated to the type system. In fact, the creator of Diesel has an entire talk where he shows how far Diesel pushes the type system and what benefits it has.

Turning back to your question, it’s impossible to store an And<_, _> in the same location as an And<And<_, _>, _> because they will have different sizes and are in fact different types. At the root, this is the same as trying to store an integer in a boolean.

In fact, there’s no way to know what concrete type you need because you don’t even know how many conditions you will have — it depends on the size of the vector.

In this case, we have to give up static dispatch and move to dynamic dispatch via a trait object. Diesel has a specific trait for this case (which also has good examples): BoxableExpression.

The remaining piece is to convert your authors to like expressions and combine them. We need a base case, however, for when authors is empty. We construct a trivially true statement (author = author) for that.

#[macro_use]
extern crate diesel;

use diesel::SqliteConnection;
use diesel::prelude::*;
use diesel::sql_types::Bool;

mod schema {
    table! {
        ebook (id) {
            id -> Int4,
            author -> Text,
        }
    }
}

fn get_books(authors: Vec<String>, connection: SqliteConnection) {
    use schema::ebook::dsl::*;

    let always_true = Box::new(author.eq(author));
    let query: Box<BoxableExpression<schema::ebook::table, _, SqlType = Bool>> = authors
        .into_iter()
        .map(|name| author.like(format!("%{}%", name)))
        .fold(always_true, |query, item| {
            Box::new(query.and(item))
        });

    ebook
        .filter(query)
        .load::<(i32, String)>(&connection)
        .expect("Error loading ebook");
}

fn main() {}

I also wouldn’t be surprised if there wasn’t a better SQL way of doing this. It appears that PostgreSQL has the WHERE col LIKE ANY( subselect ) and WHERE col LIKE ALL( subselect ) forms, for example.

Leave a Comment