1

Forgive in advance for the bad title. I will try to be clear in the description.

I am making an application that requires to work with tokio_postresql and tiberius. I need to provide query parameters for both connectors. This are their signatures.

postgresql

tokio_postgres::client::Client
pub async fn query<T>(&self, statement: &T, params: &[&dyn ToSql + Sync]) -> Result<Vec<Row>, Error>

tiberius

tiberius::query::Query
pub fn bind(&mut self, param: impl IntoSql<'a> + 'a)

As you may observe, tokio_postres admits a reference to an array a trait objets, which is really convenient. But, my bottleneck is with the param of tiberius.

Here's my code:

#[async_trait]
pub trait Transaction<T: Debug> {
    /// Performs the necessary to execute a query against the database
    async fn query<'a>(stmt: String, params: &'a [&'a (dyn QueryParameters<'a> + Sync)], datasource_name: &'a str) 
        -> Result<DatabaseResult<T>, Box<(dyn std::error::Error + Sync + Send + 'static)>>
    {
        let database_connection = if datasource_name == "" {
            DatabaseConnection::new(&DEFAULT_DATASOURCE.properties).await
        } else { // Get the specified one
            DatabaseConnection::new(
                &DATASOURCES.iter()
                .find( |ds| ds.name == datasource_name)
                .expect(&format!("No datasource found with the specified parameter: `{}`", datasource_name))
                .properties
            ).await
        };

        if let Err(_db_conn) = database_connection {
            todo!();
        } else {
            // No errors
            let db_conn = database_connection.ok().unwrap();
             
            match db_conn.database_type {
                DatabaseType::PostgreSql => {
                    let mut m_params: Vec<&(dyn ToSql + Sync)> = Vec::new();
                    for p in params.iter() {
                        m_params.push(&p as &(dyn ToSql + Sync))
                    }
                    postgres_query_launcher::launch::<T>(db_conn, stmt, params).await
                },
                DatabaseType::SqlServer =>
                    sqlserver_query_launcher::launch::<T>(db_conn, stmt, params).await
            }
        }
    }
}

where QueryParameters:

pub trait QueryParameters<'a> {}

impl<'a> QueryParameters<'a> for i32 {}
impl<'a> QueryParameters<'a> for i64 {}
impl<'a> QueryParameters<'a> for &'a str {}
impl<'a> QueryParameters<'a> for String {}
impl<'a> QueryParameters<'a> for &'a String {}
impl<'a> QueryParameters<'a> for &'a [u8] {}

impl<'a> QueryParameters<'a> for &'a (dyn ToSql + Sync + Send) {}
impl<'a> QueryParameters<'a> for &'a dyn IntoSql<'a> {}

1st question:

  • I want to cast the &'a dyn QueryParameters<'a> to &'a (dyn ToSql + Sync). Is this possible to cast from some trait to another?

2nd question:

  • The .bind() method of the tiberius client, only accept values that impl IntoSql<'a>. But I need to mix in my collection different values that already implements IntoSql<'a, but they have different type. I would like to know how to... cast??? those values of type &'a dyn QueryParameters<'a> to the values accepted by the function.

Are those things possible?

NOTE: The launch method from both modules are just a wrapper over the method calls provided above, but they accept as parameter params: &'a[&'a dyn QueryParameters<'a>]

Edit:

pub async fn launch<'a, T>(
        db_conn: DatabaseConnection,
        stmt: String,
        params: &'a [&'a dyn QueryParameters<'a>],
    ) -> Result<DatabaseResult<T>, Box<(dyn std::error::Error + Send + Sync + 'static)>> 
        where 
            T: Debug
    {
        let mut sql_server_query = Query::new(stmt);
        params.into_iter().for_each( |param| sql_server_query.bind( param ));

        let client: &mut Client<TcpStream> = &mut db_conn.sqlserver_connection
            .expect("Error querying the SqlServer database") // TODO Better msg
            .client;

        let _results: Vec<Row> = sql_server_query.query(client).await?
            .into_results().await?
            .into_iter()
            .flatten()
            .collect::<Vec<_>>();

        Ok(DatabaseResult::new(vec![]))
    }

that's the more conflictive part for me. .bind(impl IntoSql<'a> + 'a), so I should call this method for every parameter that I want to bind. I would like to cast ' &dyn QueryParameters<'a> to impl ..., but I don't know if that's is even possible.

But, if I change the method signature to:

pub async fn launch<'a, T>(
        db_conn: DatabaseConnection,
        stmt: String,
        params: &'a [impl IntoSql<'a> + 'a],
    ) -> Result<DatabaseResult<T>, Box<(dyn std::error::Error + Send + Sync + 'static)>> 

I just only can accept values of the same type. Imagine a insert query, for example. I need to be flexible to accept both i32, i64, &str... depending on the column type. So this isn't valid for my case.

Edit 2

I've found a way to solve the postgres side of the issue.

trait AsAny {
    fn as_any(&self) -> &dyn std::any::Any;
}
impl AsAny for i32 {
    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

pub trait QueryParameters<'a> {
    fn as_postgres_param(&self) -> &(dyn ToSql + Sync + 'a);
}

impl<'a> QueryParameters<'a> for i32 {
    fn as_postgres_param(&self) -> &(dyn ToSql + Sync + 'a) {
        let a: Box<&dyn AsAny> = Box::new(self);
        match a.as_any().downcast_ref::<i32>() {
            Some(b) => b,
            None => panic!("Bad conversion of parameters"),
        }
    }
}

I don't know if it's elegant, or harms performance (sure it does), but I can write now:

let mut m_params: Vec<&(dyn ToSql + Sync)> = Vec::new();
for param in params {
    m_params.push(param.as_postgres_param());
}

let query_result = client.query(&stmt, m_params.as_slice()).await;

But I can't figure out still how to work with the impl IntoSql<'a> + 'a of tiberius

Alex Vergara
  • 1,766
  • 1
  • 10
  • 29
  • I don't really understand question 2. What do you mean by "types that implemtns `IntoSql` but you cannot bind them beforehand"? – Chayim Friedman Sep 18 '22 at 13:08
  • That method accepts any type that `impl IntoSql<'a>`, in order to "bind" the parameters to the query. I would need to convert the `&dyn QueryParameters<'a>` into something that fits into the `impl IntoSql<'a>`. Edited `beforehand` – Alex Vergara Sep 18 '22 at 13:23
  • Also, a way of thinking about the second question is if it's possible to (cast, convert?) the `&dyn QueryParameters<'a>` to `impl IntoSql<'a>` – Alex Vergara Sep 18 '22 at 13:26
  • I have trouble to answer you, the question is too complex it's come with too much complicated dependencies, I would guess you are doing it wrong. You probably should have a vec with your concrete type and call the two function with it instead of wanted to have something magic that work for both. You are looking for something too complex – Stargateur Sep 18 '22 at 13:28
  • 1
    The `IntoSql` is problematic as it is by value and can't support `dyn` directly. – Chayim Friedman Sep 18 '22 at 13:30
  • Can you create a more minimal example, something we can run? – Chayim Friedman Sep 18 '22 at 13:32
  • It's a large macro based codebase. I guess that the point of @Stargateur solves the half of my problem. I've already though about that, but I was trying to find a better solution. Even tho, if I choose to split both calls (one for postgres, one for tiberius), I still don't know how to pass the query parameters in a datastructure that supports i32, i64, &str,,, to be able to use the tiberius bind method. I updated the question to reflect the last problem. – Alex Vergara Sep 18 '22 at 14:47
  • @ChayimFriedman I found how to solve the `postgres` side of the trait objects cast, edited to reflect it – Alex Vergara Sep 18 '22 at 15:30

1 Answers1

4

Essentially, you need a &dyn QueryParameter to work as both a &dyn ToSql and an impl IntoSql, right? Lets start from scratch:

trait QueryParameter {}

The &dyn ToSql part is easy since you can use the trick shown in this answer. You need your QueryParameter trait to have an associated function to convert from &self to &dyn Sql. Like so:

trait QueryParameter {
    fn as_to_sql(&self) -> &dyn ToSql;

The impl IntoSql is trickier since consuming trait objects is a dicey affair. However, to implement the trait, we only need to construct a ColumnData. And we'll see in a second that its just that simple:

trait QueryParameter {
    fn as_column_data(&self) -> ColumnData<'_>;

because we can next implement IntoSql for &dyn QueryParameter like I mentioned in your other question:

impl<'a> IntoSql<'a> for &'a dyn QueryParameter {
    fn into_sql(self) -> ColumnData<'a> {
        self.as_column_data()
    }
}

And besides implementation for QueryParameter itself, that's it! We need to sprinkle in some Sync since ToSql and IntoSql require them, but this is a (mostly) working example:

use tiberius::{ColumnData, IntoSql, Query};
use tokio_postgres::types::ToSql;

trait QueryParameter: Sync {
    fn as_to_sql(&self) -> &(dyn ToSql + Sync);
    fn as_column_data(&self) -> ColumnData<'_>;
}

impl QueryParameter for i32 {
    fn as_to_sql(&self) -> &(dyn ToSql + Sync) { self }
    fn as_column_data(&self) -> ColumnData<'_> { ColumnData::I32(Some(*self)) }
}

impl QueryParameter for i64 {
    fn as_to_sql(&self) -> &(dyn ToSql + Sync) { self }
    fn as_column_data(&self) -> ColumnData<'_> { ColumnData::I64(Some(*self)) }
}

impl QueryParameter for &'_ str {
    fn as_to_sql(&self) -> &(dyn ToSql + Sync) { self }
    fn as_column_data(&self) -> ColumnData<'_> { ColumnData::String(Some((*self).into())) }
}

impl QueryParameter for String {
    fn as_to_sql(&self) -> &(dyn ToSql + Sync) { self }
    fn as_column_data(&self) -> ColumnData<'_> { ColumnData::String(Some(self.into())) }
}

impl<'a> IntoSql<'a> for &'a dyn QueryParameter {
    fn into_sql(self) -> ColumnData<'a> {
        self.as_column_data()
    }
}

async fn via_tiberius(stmt: &str, params: &[&dyn QueryParameter]) {
    let mut client: tiberius::Client<_> = todo!();
    let mut query = Query::new(stmt);
    for &param in params {
        query.bind(param)
    }
    let _ = query.execute(&mut client).await;
}

async fn via_tokio_postgres(stmt: &str, params: &[&dyn QueryParameter]) {
    let client: tokio_postgres::Client = todo!();
    let params: Vec<_> = params.iter().map(|p| p.as_to_sql()).collect();
    let _ = client.query(stmt, &params).await;
}
Alex Vergara
  • 1,766
  • 1
  • 10
  • 29
kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • That's just perfect. I spent all my day playing with `std::any::Any`, even with `std::mem::transmute` (bad things happened), and the solution was just right in front of my eyes. I can't explain you how happy I am now. Thanks! – Alex Vergara Sep 18 '22 at 23:27
  • Btw, the impl of the String, does not need `self.as_str().into`. `.into()` already performs the conversion – Alex Vergara Sep 18 '22 at 23:28
  • I just tried now the solution on the real project and works perfectly. Lot of thanks again @kmdreko – Alex Vergara Sep 19 '22 at 16:12