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.