Diesel conditional query builder?


#1

I’m trying to create a conditional query builder for diesel query and having a hard time with a generics here, since diesel::query_builder::BoxedSelectStatement<ST, QS, DB> is generic over a few parameters.

While I can inline it all into a web controller function, resulting code will bloat drastically, so I want to wrap all that logic into a some kind of builder. Problem is: diesel query methods returns new BoxedSelectStatement type objects with different ST and QS types, and I don’t know them at compilation time, since they can vary based on the already joined tables and selected fields.

Example table definitions

table! {
    table1 {
        id -> Integer,
        title -> String,
    }
}

table! {
    table2 {
        id -> Integer,
        table1_id -> Integer,
        tag -> String,
    }
}

table! {
    table3 {
        id -> Integer,
        table1_id -> Integer,
        tag -> String,
    }
}

table! {
    table4 {
        id -> Integer,
        table1_id -> Integer,
        tag -> String,
    }
}

allow_tables_to_appear_in_same_query!(table1, table2, table3, table4);

Following code is not compiling (obviously), but I hope, it will explain my thoughts.

Builder

use std::boxed::Box;

use diesel::*;
use diesel::mysql::Mysql;
use diesel::query_dsl::*;
use diesel::query_builder::BoxedSelectStatement;

use schema::{table1, table2, table3, table4};

struct Builder<'b, ST, QS>(Box<BoxedSelectStatement<'b, ST, QS, Mysql>>);

impl<'b, ST, QS> Builder<'b, ST, QS> {
    pub fn new() -> Builder<'b, ST, QS> {
        let inner = table1::table.into_boxed();

        Builder(Box::new(inner))
    }

    pub fn with_table2(self, tag: String) -> Builder<'b, ST, QS> {
        let inner = self.0.join(table2::table.on(
            table1::id.eq(table2::table1_id)
                .and(table2::tag.eq(tag))
        ));

        Builder(Box::new(inner))
    }

    pub fn with_table3(self, tag: String) -> Builder<'b, ST, QS> {
        let inner = self.0.join(table3::table.on(
            table1::id.eq(table3::table1_id)
                .and(table3::tag.eq(tag))
        ));

        Builder(Box::new(inner))
    }

    pub fn with_table4(self, tag: String) -> Builder<'b, ST, QS> {
        let inner = self.0.join(table4::table.on(
            table1::id.eq(table4::table1_id)
                .and(table4::tag.eq(tag))
        ));

        Builder(Box::new(inner))
    }

    pub fn finish(self) -> BoxedSelectStatement<'b, ST, QS, Mysql> {
        self.0
    }
}

Usage example

fn main() {
    let query = QueryParams::from_string("?foo=tag2&baz=tag4");
    let builder = Builder::new();

    if query.contains("foo") {
        let builder = builder.with_table2(query.get("foo"));
    }
    if query.contains("bar") {
        let builder = builder.with_table3(query.get("bar"));
    }
    if query.contains("baz") {
        let builder = builder.with_table4(query.get("baz"));
    }

    let sql_query = builder.finish();
    sql_query.load(&connection).unwrap();
}

For example, new() method will following type:

diesel::query_builder::BoxedSelectStatement<'_, (diesel::sql_types::Integer, std::string::String), db::table1::table, Mysql>

, and if I call with_table2() method later, I’ll get

diesel::query_builder::BoxedSelectStatement<'_, (diesel::sql_types::Integer, std::string::String), diesel::query_source::joins::JoinOn<diesel::query_source::joins::Join<db::table1::table, _, _>, _>, diesel::mysql::Mysql>

So, since BoxedSelectStatement is new each time, is there is some way to either ignore generic types at the returned type or move that checks into runtime?
One option was to hardcore these types into a builder method signatures, but I don’t know them until runtime, since tables can be joined in a random order.


#2

Not sure if these diesel types will facilitate this, but you’ll need to employ type erasure (i.e. trait objects). The issue you’re having is essentially equivalent to using the various Iterator or Future combinators that build up different concrete types; trait objects are used there to add dynamic behavior.

@sgrif or @killercup will know better.


#3

Probably I failed to explain that properly, but BoxedSelectStatement is a struct, not a trait, so I’m not sure that type erasure is even applicable here.


#4

Yeah, I understood that part. The types returned in the Iterator and Future combinators are also (generic) structs.

What I meant was you may need to find a way to abstract over those concrete generic types using type erasure, and working with trait objects only (ala Box<Iterator<Item=...>> or Box<Future<Item=...,Error=...>>). If the query execution ultimately needs some trait, and there’s an impl of that trait for a boxed version, it may work. But, I don’t know diesel’s API well enough to say for sure.