1

I am trying to write a function which takes a pointer to a function which executes some SQL query nested in a transaction. I have been running into errors with this for a day now and I do not know how to solve it.

Minimum Reproducible Example

Rust Playground Link

use core::future::Future;

// Allows execution of a set of queries.
struct Transaction(pub usize);
// A single SQL query string.
struct Query(pub &'static str);

impl Query {
    // Execute the query without returning anything.
    pub async fn execute_without_return(&self, tx: &mut Transaction) {
        tx.0 += 1;
        println!("{}", self.0);
    }
    
    // Execute the query and return a result.
    pub async fn execute_with_return(&self, tx: &mut Transaction) -> usize {
        tx.0 += 1;
        println!("{}", self.0);
        return 123;
    }
}

// Execute some query between two other queries to set and reset the user role.
async fn query_as_user<Fut>(query_fn: fn(&mut Transaction) -> Fut) -> usize 
where
  Fut: Future<Output = usize>
{
    let mut tx = Transaction(0);
    Query("SET ROLE user;").execute_without_return(&mut tx).await;
    let result = query_fn(&mut tx).await;
    Query("RESET ROLE;").execute_without_return(&mut tx).await;
    result
}

async fn select_all(tx: &mut Transaction) -> usize {
    Query("SELECT * FROM table;").execute_with_return(tx).await
}

#[tokio::main]
async fn main() {
    let res = query_as_user(select_all).await;
    println!("\nResult: {}", res)
}

If you run the code as is, it will show an error:

error[E0308]: mismatched types
  --> src/main.rs:41:29
   |
41 |     let res = query_as_user(select_all).await;
   |               ------------- ^^^^^^^^^^ one type is more general than the other
   |               |
   |               arguments to this function are incorrect
   |
   = note: expected fn pointer `for<'a> fn(&'a mut Transaction) -> _`
                 found fn item `for<'a> fn(&'a mut Transaction) -> impl Future<Output = usize> {select_all}`
   = note: when the arguments and return types match, functions can be coerced to function pointers
note: function defined here
  --> src/main.rs:24:10
   |
24 | async fn query_as_user<Fut>(query_fn: fn(&mut Transaction) -> Fut) -> usize 
   |          ^^^^^^^^^^^^^      -------------------------------------

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` (bin "playground") due to previous error

With some lifetime annotation finagling I can get a different error - which is:

error[E0521]: borrowed data escapes outside of async closure
error[E0499]: cannot borrow `tx` as mutable more than once at a time

This error is coming out of the non-toy program but is essentially pointing to let result = query_fn(&mut tx).await; and claiming that the mutable reference to tx is not valid.

In the real program I am also trying to make this general over the return type, but this is the core of the problem.

Note: I am using the sqlx to make the queries, hence the structure of Query and Transaction.

I am expecting to be able to write the query_as_user function to accept any Query + execution method (e.g. return one row, multiple rows, nothing...). It should execute the query using the method defined in the query function, nested in the transaction which sets then resets the user role.

Yuri Astrakhan
  • 8,808
  • 6
  • 63
  • 97
Bryan Reilly
  • 114
  • 4

2 Answers2

1

Found a method to do this using async traits.

Shown using actual sqlx types:

trait AsUserQueryFn<Args, Out> {
    async fn call(
        &mut self,
        tx: &mut Transaction<'_, Postgres>,
        args: Args,
    ) -> Result<Out, sqlx::Error>;
}

async fn execute_user_level_query<'a, Args, Out>(
    &'a self,
    as_user: UserTableRow,
    input: Args,
    mut execution_fn: impl AsUserQueryFn<Args, Out>,
) -> Result<Out, UserLevelQueryError> {
  ...
}

Bryan Reilly
  • 114
  • 4
1

The problem lies in not being able to mark the output of the query function with the lifetime of its argument. It would be nice to do something like this.

async fn query_as_user<Fut>(query_fn: for<'a> fn(&'a mut Transaction) -> Fut) -> usize 
where
  Fut: Future<Output = usize> + 'a

But that's not valid. I've made a workaround that requires a boxed trait object. (playground)

use core::pin::Pin;role.
async fn query_as_user<F>(query_fn: F) -> usize
where
    F: for<'a> FnOnce(&'a mut Transaction) -> Pin<Box<dyn Future<Output = usize> + 'a>>,
{
    // same stuff
}

fn select_all(tx: &mut Transaction) -> Pin<Box<dyn Future<Output = usize> + '_>> {
    Box::pin(Query("SELECT * FROM table;").execute_with_return(tx))
}

I've also upgraded from fn to FnOnce since FnOnce is more general. You may need FnMut or Fn. You also may need to add Send or Sync to the trait object. Note that it is fully impossible to put the generated future into tokio::spawn by the regular lifetime rules.

drewtato
  • 6,783
  • 1
  • 12
  • 17