rustlifetimetokio-postgres

Lifetime on returned trait not present on tokio_postgres Row and does not "live long enough"


I am looking for understanding on how to resolve a lifetime issue in Rust. There are many similar questions on SO, ofc, but none of them seem exactly the same -- all the others seem to be about structs or 'static, mine is about a trait that I am using from an external library, with an open ended lifetime.

I am using tokio_postgres to query rows from the database, as one does. I am trying to streamline some operations I do often. Getting a collection of UUIDs from a query is easy:

pub fn rows_as_first(rows: Vec<Row>) -> Vec<Uuid> {
    rows.into_iter().map(|row| row.get(0)).collect()
}

That works great. But when I try to make this generic (e.g. to collect Strings or something):

pub fn rows_as_first_generic<'a, T: FromSql<'a>>(rows: Vec<Row>) -> Vec<T> {
    rows.into_iter().map(|row| row.get(0)).collect()
}

I get an error:

error[E0597]: `row` does not live long enough
  --> backend-axum/src/queries.rs:64:32
   |
63 | pub fn rows_as_first_generic<'a, T: FromSql<'a>>(rows: Vec<Row>) -> Vec<T> {
   |                              -- lifetime `'a` defined here
64 |     rows.into_iter().map(|row| row.get(0)).collect()
   |                           ---  ^^^-------
   |                           |    |        |
   |                           |    |        `row` dropped here while still borrowed
   |                           |    borrowed value does not live long enough
   |                           |    argument requires that `row` is borrowed for `'a`
   |                           binding `row` declared here

Now, I definitely understand that row only lives for the length of the closure, that makes total sense. What I don't get is:

  1. What is different about the Uuid version of this function that it does not complain? I assume there is some trait that makes it magically work, and I'd like to know a) what it is and b) how to discover such information in the future when trying to refactor code like this in rust. (This is not the first time I've run into similar issues when refactoring.)

  2. How am I supposed to add a lifetime to a reference that a) is not a reference, because I own it and b) when I don't want a reference at all, I want to own the end result as well.

I am suspicious of the T: FromSql<'a> -- perhaps it needs some further clarification. I need it for the get(0) call, and what I am returning is, by definition, some value coming from a sql query. But I don't want a reference, I want to own the end result. I am happy to do a clone() to make that happen, it just doesn't seem like that's the problem here. (I tried adding clone() of course.)

So, somehow, I am supposed to have a lifetime specifier on row, but the way tokio_postgres works is:

client.query(&stmt, params).await?

Returns Vec<Row> with no lifetime included because it is owned. So... what am I supposed to do? And how does the Uuid example work? I am sure there is something foundational and obvious that I am missing here, so... sorry.

And thanks.


Solution

  • Great questions! Let me try answering your second question first. You're right that the core of it is T: FromSql<'a>. This signature:

    pub fn rows_as_first_generic<'a, T: FromSql<'a>>
    

    has 'a as an input lifetime, which means the signature says: "for any lifetime 'a chosen by the caller of rows_as_first_generic, T must be creatable from a postgres values that lives for 'a". This is not what you want: you want a T which is creatable from a postgres value that lives for a lifetime that rows_as_first_generic chooses (not its caller) -- ie the lifetime of the get call you mention in the body of your function. The way you write this in rust is:

    pub fn rows_as_first_generic<T: for<'a> FromSql<'a>>
    

    This says: T must be creatable from an postgres value with any lifetime. (ie T must implement FromSql<'a> for any lifetime 'a). And so, in particular, it is creatable from the postgres value that lives as long as your row does.

    For your first question of why Uuid works and how to discover the difference: The reason it works for Uuid is that Uuid does implement FromSql for any lifetime (as you can see in the tokio_postgres docs).

    I'm not sure I have much to suggest other than practice with these patterns and building intuition about what errors suggest which solutions. In this case, if a trait bound requires some temporary value to be borrowed for some input lifetime (which is the error you get above) it's sometimes because you really want a HRTB as in this example. An alternative would be to take a &'a Vec<Row> or similar, in which case you could use an input lifetime. ie this also works:

    pub fn rows_as_first_generic_input<'a, T: FromSql<'a>>(rows: &'a Vec<Row>) -> Vec<T>
    

    Because now the caller is guaranteeing you can hold on to the Row for the same length of time as T needs it around (as opposed to the short temporary lifespan from your original code). Hope that is helpful!