Search code examples
rustlifetimerust-sqlx

Handling lifetimes when decoding generic types using SQLx


I am attempting to use a generic method to retrieve values from database tables that are structurally identical, but have different types for one of their columns. Simplified example below

async fn query<'a, 'r, T: DatabaseType<Item=T> + Decode<'r, Sqlite> + Type<Sqlite>>(&self, name: &'a str) -> Result<Vec<NamedValue<'a, T>>> {
        let mut connection = self.pool.acquire().await?;

        let mut rows = sqlx::query("Select id, value from table where name = $1")
            .bind(name)
            .fetch(&mut connection);

        let mut results = Vec::new();
        while let Some(row) = rows.try_next().await? {
            results.push(NamedValue {
                name,
                value: row.try_get("value")?
            })
        }

        Ok(results)
    }

This will not compile, with the error: borrowed value does not live long enough, argument requires that 'row' is borrowed for 'r. The lifetime sqlx::Decode wants ('r), has to be declared as part of the query function's signature, but the resource the lifetime refers to does not exist yet, and only exists when the query executes and the stream is iterated over. I can't omit this bound on the generic, because the type does need to be decodable for try_get to work, so how do I tell the compiler that it is actually completely safe, and that the decoding is happening against a row that will definitely live longe enough for the try_get? Once the value is decoded, it will always have a static lifetime.

Rust playground doesn't include SQLx, an example that can be compiled at home is below:

[package]
name = "sqlx-minimal-example"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "sqlite"] }
anyhow = "1.0"
futures = "0.3"

And the full application would be:

use anyhow::Result;
use sqlx::{Decode, Row, Sqlite, SqlitePool, Type};
use futures::TryStreamExt;

#[tokio::main]
async fn main() -> Result<()> {
    println!("Hello, world!");
    Ok(())
}

struct NamedValue<'a ,T> {
    name: &'a str,
    value: T
}

struct SqliteBackend {
    pool: SqlitePool
}

trait DatabaseType {
    type Item;
}

impl DatabaseType for f32 {
    type Item = f32;
}

impl DatabaseType for i32 {
    type Item = i32;
}

impl SqliteBackend {
    async fn query<'a, 'r, T: DatabaseType<Item=T> + Decode<'r, Sqlite> + Type<Sqlite>>(&self, name: &'a str) -> Result<Vec<NamedValue<'a, T>>> {
        let mut connection = self.pool.acquire().await?;

        let mut rows = sqlx::query("Select id, value from table where name = $1")
            .bind(name)
            .fetch(&mut connection);

        let mut results = Vec::new();
        while let Some(row) = rows.try_next().await? {
            results.push(NamedValue {
                name,
                value: row.try_get("value")?
            })
        }

        Ok(results)
    }
}

Solution

  • Higher-ranked trait bounds were the answer. This tells the compiler the type is decodable for all possible lifetimes.

    Working function below:

    async fn query<'a, T: DatabaseType<Item=T> + for<'r> Decode<'r, Sqlite> + Type<Sqlite>>(&self, name: &'a str) -> Result<Vec<NamedValue<'a, T>>> {
            let mut connection = self.pool.acquire().await?;
    
            let mut rows = sqlx::query("Select id, value from table where name = $1")
                .bind(name)
                .fetch(&mut connection);
    
            let mut results = Vec::new();
            while let Some(row) = rows.try_next().await? {
                results.push(NamedValue {
                    name,
                    value: row.try_get("value")?
                })
            }
    
            Ok(results)
        }