Please help with borrow checker

I'm trying to write a simple SQL query builder and I have an issue with the borrow checker. Here is my simplified code

fn main() {
    let mut c = QueryBuilder {
        query: "select ...".to_string(),
        query_arguments: Some(Box::new(34)),
    };

    let a = AQuery {
        query_string: &c.query,
    };

    c.query_arguments.take().unwrap().bind(a);
}

// This struct is provided by a  3rd party library. I cannot change the definition.
struct AQuery<'a> {
    query_string: &'a str,
}

trait Bindable1<T> {
    fn bind(self: Box<Self>, t: T) -> T;
}

impl<'a> Bindable1<AQuery<'a>> for i64 {
    fn bind(self: Box<Self>, t: AQuery<'a>) -> AQuery<'a> {
        // simplified
        t
    }
}

struct QueryBuilder<T> {
    query: String,
    query_arguments: Option<Box<dyn Bindable1<T>>>,
}

The compiler complains with

error[E0597]: `c.query` does not live long enough
  --> src/main.rs:64:23
   |
64 |         query_string: &c.query,
   |                       ^^^^^^^^ borrowed value does not live long enough
...
68 | }
   | -
   | |
   | `c.query` dropped here while still borrowed
   | borrow might be used here, when `c` is dropped and runs the destructor for type `QueryBuilder<AQuery<'_>>`

The question is how to design the QueryBuilder to contain the query and the query arguments?

Is the main function exactly as posted here? Local variables should get dropped in reverse order that they are declared, so a should get dropped before c, so that error shouldn't happen unless the declaration order is different.

Yes, the main function is exactly as posted. I use rustc 1.56.1 (59eed8a2a 2021-11-01).

Ok, I see the issue. It's rather subtle. The problem is that the type of c is going to be QueryBuilder<AQuery<'a>> for some specific lifetime 'a. Since a type can never be annotated with a smaller lifetime than the region in which the object exists, this forces that lifetime 'a to be at least the region until after c is dropped. However, then you pass a.query_string to bind, which takes an AQuery<'a>. Thus, the type of a must also be AQuery<'a> for a lifetime 'a that extends until after the destructor of c. Therefore, a is borrowing the c.query field until after the destructor of c even though a itself is dropped before c is. (There's no rule that you can't borrow stuff for longer than you exist.)

To fix this, you will need to redesign something so that QueryBuilder is not forced to pick a single specific lifetime. For example, this would work:

struct QueryBuilder {
    query: String,
    query_arguments: Option<Box<dyn for<'a> Bindable1<AQuery<'a>>>>,
}

The above syntax requires that the type in the Box implements the following infinite list of traits:

  • Bindable1<AQuery<'lifetime1>>
  • Bindable1<AQuery<'lifetime2>>
  • Bindable1<AQuery<'lifetime3>>
  • ...
  • Bindable1<AQuery<'static>>

The above list goes through every possible lifetime. This will work because the lifetime annotated on a is no longer also annotated on c, so it is no longer required to last until after the destructor of 'c.

2 Likes

I see, thank you. Is it possible to have QueryBuilder parameterized over T, i.e. QueryBuilder<T>? So that QueryBuilder is not bound to a specific implementation of Query and can be potentially used with any SQL libraries?

Not in the way you're doing it here anyway.

How would you implement it? I would like to make the QueryBuilder contain the query and the arguments that can be then late bound to some Query implementation. In my special case sqlx the Query has a lifetime Query<'q>.

I would probably just remove the Box and do this:

fn main() {
    let mut c = QueryBuilder {
        query: "select ...".to_string(),
        query_arguments: Some(Box::new(34)),
    };

    let a = AQuery {
        query_string: &c.query,
    };

    c.query_arguments.take().unwrap().bind(a);
}

// This struct is provided by a  3rd party library. I cannot change the definition.
struct AQuery<'a> {
    query_string: &'a str,
}

trait Bindable1<T> {
    fn bind(self: Box<Self>, t: T) -> T;
}

impl<'a> Bindable1<AQuery<'a>> for i64 {
    fn bind(self: Box<Self>, t: AQuery<'a>) -> AQuery<'a> {
        // simplified
        t
    }
}

struct QueryBuilder<T> {
    query: String,
    query_arguments: Option<T>,
}

Sorry, it was not quite correct in the code sample. I oversimplified the example. The actual QueryBuilder contains a vec of any type that are bindable to the query implementation. So correct would be

struct QueryBuilder<T> {
    query: String,
    query_arguments: Vec<Box<dyn Bindable1<T>>>,
}

I need this dynamic dispatch to be able to call bind(query) on every type in the vector.

Why does bind return the value back?

I took it only because of the original method signature in sqlx

I would probably use an enum over the possible kinds of values as the value in your vector, making QueryBuilder entirely non-generic.

1 Like

I see. The downside is that I would need to manually track all possible value types for every query implementation. But it seems to be robust.

Thank you for your detailed explanation!

I don't think there are that many possible value types. It's very doable.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.