Search code examples
rustrust-sqlx

Storing disparate SQLX types in a vector


As the title suggests, I want to store various SQLX types in a vector for manual query building. Here is my best stab at it:

use sqlx::{Database, Encode, Type};
use sqlx::database::HasArguments;
use sqlx::query::QueryScalar;

pub type SqlVec<'q, DB: Database> = Vec<Box<dyn SqlTrait<'q, DB>>>;

pub trait SqlTrait<'q, DB: Database>: Send + Encode<'q, DB> + Type<DB> {}
impl<'q, DB: Database, U> SqlTrait<'q, DB> for U where U:Send + Encode<'q, DB> + Type<DB> {}

pub trait SqlVecTrait<'q, DB: Database> {
    fn add<T: SqlTrait<'q, DB>>(&mut self, value: T);
}

impl<'q, DB: Database> SqlVecTrait<'q, DB> for SqlVec<'q, DB> {
    fn add<T: SqlTrait<'q, DB>>(&mut self, value: T) {
        self.push(Box::new(value))
    }
}

The code would be used like so:

let mut query = "SELECT * FROM stuff WHERE num = $1".to_string();
let mut args = SqlVec::new();
args.add(3);
if condition {
  query += ", code = $2";
  args.add("s");
}
let mut query = sqlx::query(&query);
for value in args.iter() {
   query = query.bind(value);
}
let rows = query.fetch(&mut db_pool);

The compiler complains that DB cannot be made into an object due to a couple of methods that don't take self. In the documentation and from my innumerable searches I have not found a way to store different types in a vector that can be passed to bind in SQLX.


Solution

  • I don't think the approach you're trying is possible, conceptually. Consider for example that to implement Type::type_info, you need to be able to return the type information out of thin air, without having access to an actual object you could do dynamic dispatch on. Hence, you can never pass anything dyn … or enumish to bind, it wants to be able to get that TypeInfo at compile-time.

    The only practical approach I can think of is to actually have separate bind calls for all concrete types of parameters you want to bind. Meaning you need your own enum with all types you want, and then bind each variant in a separate match arm, e.g. like this:

    use sqlx::{
        database::HasArguments, query::Query, Connection, Database, Encode, SqliteConnection, Type,
    };
    
    enum QueryParameter {
        String(String),
        I64(i64),
        Bool(bool),
        // Problem: Do you want to support all of rusts integer types?
        // And what about tuples?
    }
    
    pub trait SqlTrait<'q, DB: Database>: Send + Sync {}
    
    macro_rules! query_parameter_from {
        ($t:ty, $v:ident) => {
            impl From<$t> for QueryParameter {
                fn from(f: $t) -> Self {
                    Self::$v(f)
                }
            }
        };
    }
    query_parameter_from! {String, String}
    query_parameter_from! {i64, I64}
    query_parameter_from! {bool, Bool}
    
    // just an abbreviation
    trait TyEnc<'q, DB: Database>: Type<DB> + Encode<'q, DB> {}
    impl<'q, DB: Database, U: Type<DB> + Encode<'q, DB>> TyEnc<'q, DB> for U {}
    
    // Changed from a type declaration to a struct because trait+impl is too much typing
    pub struct SqlVec(Vec<QueryParameter>);
    
    impl SqlVec {
        fn add<T: Into<QueryParameter>>(&mut self, value: T) {
            self.add(value.into())
        }
    
        fn bind_to<'q, 'p, DB: Database>(
            &'q self,
            mut query: Query<'q, DB, <DB as HasArguments<'q>>::Arguments>,
        ) -> Query<'q, DB, <DB as HasArguments<'q>>::Arguments>
        where
            // This may look weird, but there might be a DB that, e.g., doesn't support strings
            String: TyEnc<'q, DB>,
            i64: TyEnc<'q, DB>,
            bool: TyEnc<'q, DB>,
        {
            for value in self.0.iter() {
                query = match value {
                    QueryParameter::String(s) => query.bind(s),
                    QueryParameter::I64(u) => query.bind(u),
                    QueryParameter::Bool(b) => query.bind(b),
                }
            }
            query
        }
    
        fn new() -> Self {
            Self(Vec::new())
        }
    }
    
    async fn foo() -> Result<(), sqlx::Error> {
        // Adding some stuff to make your example actually compile
        let mut conn = SqliteConnection::connect("sqlite::memory:").await?;
        let condition = true;
        
        let mut query = "SELECT * FROM stuff WHERE num = $1".to_string();
        let mut args = SqlVec::new();
        args.add(3);
        if condition {
            query += ", code = $2";
            args.add("s".to_string());
        }
        let query = sqlx::query(&query);
        let query = args.bind_to(query);
        let _rows = query.fetch(&mut conn);
        Ok(())
    }
    

    It seems that internally, something quite similar is going on… Maybe you can even reuse that, if you want to support only one type of database. Failing that, you might be able to make this less painful through the use of some of the any-stuff that sqlx provides, but I lack insight as to how.