3

I'm trying to design a struct to carry around a Postgres connection, transaction, and a bunch of prepared statements, and then execute the prepared statements repeatedly. But I'm running into lifetime problems. Here is what I've got:

extern crate postgres;

use postgres::{Connection, TlsMode};
use postgres::transaction::Transaction;
use postgres::stmt::Statement;

pub struct Db<'a> {
    conn: Connection,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

fn make_db(url: &str) -> Db {
    let conn = Connection::connect(url, TlsMode::None).unwrap();
    let tx = conn.transaction().unwrap();
    let insert_user = tx.prepare("INSERT INTO users VALUES ($1)").unwrap();
    Db {
        conn: conn,
        tx: tx,
        insert_user: insert_user,
    }
}

pub fn main() {
    let db = make_db("postgres://paul@localhost/t");
    for u in &["foo", "bar"] {
        db.insert_user.execute(&[&u]);
    }
    db.tx.commit().unwrap();
}

Here is the error I'm getting (on Rust 1.15.0 stable):

error: `conn` does not live long enough
  --> src/main.rs:15:14
   |
15 |     let tx = conn.transaction().unwrap();
   |              ^^^^ does not live long enough
...
22 | }
   | - borrowed value only lives until here
   |
note: borrowed value must be valid for the anonymous lifetime #1 defined on the body at 13:28...
  --> src/main.rs:13:29
   |
13 | fn make_db(url: &str) -> Db {
   |                             ^

I've read the Rust book (I've lost count how many times), but I'm not sure how to make progress here. Any suggestions?

EDIT: Thinking about this some more I still don't understand why in principle I can't tell Rust, "conn lives as long as Db does". The issue is with moving conn, but what if I don't move it? I understand why in C you can't return a pointer to stack-allocated memory, e.g.:

#include <stdio.h>

int *build_array() {
  int ar[] = {1,2,3};
  return ar;
}

int main() {
  int *ar = build_array();
  printf("%d\n", ar[1]);
}

And I get how that is similar to in Rust returning a &str or returning a vec slice.

But in Rust you can do this:

#[derive(Debug)]
struct S {
    ar: Vec<i32>,
}

fn build_array() -> S {
    let v = vec![1, 2, 3];
    S { ar: v }
}

fn main() {
    let s = build_array();
    println!("{:?}", s);
}

And my understanding is that Rust is smart enough so that returning S doesn't actually require a move; essentially it is going straight to the caller's stack frame.

So I don't understand why it can't also put Db (including conn) in the caller's stack frame. Then no moves would be required, and tx would never hold an invalid address. I feel like Rust should be able to figure that out. I tried adding a lifetime hint, like this:

pub struct Db<'a> {
    conn: Connection<'a>,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

But that gives an "unexpected lifetime parameter" error. I can accept that Rust can't follow the logic, but I'm curious if there is a reason why in principle it couldn't.

It does seem that putting conn on the heap should solve my problems, but I can't get this to work either:

pub struct Db<'a> {
    conn: Box<Connection>,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

Even with a let conn = Box::new(Connection::connect(...));, Rust still tells me "conn does not live long enough". Is there some way to make this work with Box, or is that a dead end?

EDIT 2: I tried doing this with macros also, to avoid any extra stack frames:

extern crate postgres;

use postgres::{Connection, TlsMode};
use postgres::transaction::Transaction;
use postgres::stmt::Statement;

pub struct Db<'a> {
    conn: Connection,
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

macro_rules! make_db {
      ( $x:expr ) => {
        {
          let conn = Connection::connect($x, TlsMode::None).unwrap();
          let tx = conn.transaction().unwrap();
          let insert_user = tx.prepare("INSERT INTO users VALUES ($1)").unwrap();
          Db {
            conn: conn,
            tx: tx,
            insert_user: insert_user,
          }
        }
      }
    }


pub fn main() {
    let db = make_db!("postgres://paul@localhost/t");
    for u in &["foo", "bar"] {
        db.insert_user.execute(&[&u]);
    }
    db.tx.commit().unwrap();
}

But that still tells me that conn does not live long enough. It seems that moving it into the struct should really not require any real RAM changes, but Rust still won't let me do it.

Community
  • 1
  • 1
Paul A Jungwirth
  • 23,504
  • 14
  • 74
  • 93
  • See http://stackoverflow.com/questions/32209391/how-to-store-a-sqliteconnection-and-sqlitestatement-objects-in-the-same-struct-i for a close approximation to your question, which prompted the question this was marked as a duplicate of. – Shepmaster Feb 10 '17 at 00:55
  • I see that http://stackoverflow.com/questions/32209391/how-to-store-a-sqliteconnection-and-sqlitestatement-objects-in-the-same-struct-i doesn't actually have an answer with working code. – Paul A Jungwirth Feb 10 '17 at 01:01
  • Also re the other question you link to: I'm not using any references in my struct. – Paul A Jungwirth Feb 10 '17 at 01:02
  • Please make sure you read the **duplicate** question thoroughly. Specifically, you just said the moral equivalent of *"Sometimes, I'm not even taking a reference of the value and I get the same error"* from the duplicate. Hint: that's what those `'a` lifetimes mean - a reference. – Shepmaster Feb 10 '17 at 01:04
  • 1
    The last line of this comment is my own experience every time I come to StackOverflow with Rust questions: https://news.ycombinator.com/item?id=13586363 – Paul A Jungwirth Feb 10 '17 at 01:04
  • Can you explain why a lifetime implies a reference? – Paul A Jungwirth Feb 10 '17 at 01:05
  • I apologize; I'm not attempting to yell at you at all. It's just that your question has already been asked, so I marked it as a duplicate. **Duplicates aren't inherently bad**, they just create more signposts for people to search and find the answers. I also pointed to to another question that asks the same thing as this one, substituting SQLite for Postgres, which likewise has an answer. As for *why a lifetime implies a reference* — that seems like a perfectly reasonable question to ask. The short version is that that's what lifetimes **are** — how long a reference is valid for. – Shepmaster Feb 10 '17 at 01:11
  • It's actually slightly more nuanced, so I've unduped. You still wont be able to do what you want, but I'll show why in a second. – Shepmaster Feb 10 '17 at 01:14
  • If a lifetime implies a reference, then is my struct the same thing as `pub struct Db<'a> { conn: Connection, tx: &'a Transaction<'a>, insert_user: &'a Statement<'a>, }`? If so, why does using that change the compiler errors? – Paul A Jungwirth Feb 10 '17 at 01:16
  • Please do not edit your question to ask follow-up questions; just go ahead and *ask a new question*. The entire point of Stack Overflow is to provide a canonical resource for questions and answers to those questions. One of the ways that is done is by having questions where the titles map to the question being asked. Your follow up question only tenuously has to deal with "a database connection bundle", so more properly lives somewhere else. – Shepmaster Feb 10 '17 at 04:42
  • I will also point once again to [the other question](http://stackoverflow.com/q/32300132/155423) which has a section that again addresses your question. You can find it by searching for **There is a special case where the lifetime tracking is overzealous: when you have something placed on the heap** – Shepmaster Feb 10 '17 at 04:42

2 Answers2

3

Starting with this function:

fn make_db(url: &str) -> Db {
    unimplemented!()
}

Due to lifetime elision, this is equivalent to:

fn make_db<'a>(url: &'a str) -> Db<'a> {
    unimplemented!()
}

That is, the lifetimes of all the references inside the Db struct must live as long as the string slice passed in. That only makes sense if the struct is holding on to the string slice.


To "solve" that, we can try to separate the lifetimes:

fn make_db<'a, 'b>(url: &'a str) -> Db<'b> {
    unimplemented!()
}

Now this makes even less sense because now we are just making up a lifetime. Where is that 'b coming from? What happens if the caller of make_db decides that the concrete lifetime for the generic lifetime parameter 'b should be 'static? This is further explained in Why can't I store a value and a reference to that value in the same struct?, search for "something is really wrong with our creation function".

We also see the part of the question with "Sometimes, I'm not even taking a reference of the value" in the other question, which says in the answer:

the Child instance contains a reference to the Parent that created it,

If we check out the definition for Connection::transaction:

fn transaction<'a>(&'a self) -> Result<Transaction<'a>>

or the definition if you don't believe the docs:

pub struct Transaction<'conn> {
    conn: &'conn Connection,
    depth: u32,
    savepoint_name: Option<String>,
    commit: Cell<bool>,
    finished: bool,
}

Yup, a Transaction keeps a reference to its parent Connection. Now that we see that Transaction has a reference to Connection we can return to the other question to see how to solve the problem: split apart the structs so that the nesting mirrors the lifetimes.

This was a very long-winded way of saying: no, you cannot create a single structure that contains a database and a transaction of that database due to the implementation of the postgres crate. Presumably the crate is implemented in this fashion for maximum performance.


I don't see why [returning Db<'b>] makes less sense. Normally when a function returns a thing, the thing lives as long as it is assigned to something. Why can't -> Db work the same way?

The entire point of references is that you don't own the referred-to value. You return Db and the caller of make_db would own that, but what owns the thing that Db is referring to? Where did it come from? You cannot return a reference to something local as that would violate all of Rust's safety rules. If you want to transfer ownership, you just do that.

See also

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 1
    So from your linked answer and http://stackoverflow.com/questions/36230710/how-does-rust-move-stack-variables-that-are-not-copyable I see that when the Rust docs say "moved", they mean not just "moved to a new owner" but also "moved to a new place in RAM". That is a new piece of information to me that is pretty helpful. – Paul A Jungwirth Feb 10 '17 at 01:35
  • "Now this makes even less sense because now we are just making up lifetime." --- I don't see why it makes less sense. Normally when a function returns a thing, the thing lives as long as it is assigned to something. Why can't `-> Db` work the same way? – Paul A Jungwirth Feb 10 '17 at 01:36
  • Yes, "lifetime" is probably more properly stated as "the scope of the program where the address of a value is guaranteed not to change", but no one would say that. – Shepmaster Feb 10 '17 at 01:37
  • 1
    Okay, I really appreciate your patience and helpfulness. I wrote an answer myself that shows working code close to what I want. There are still concepts I don't understand yet, but I'm working on it. In particular: since my struct has no references, doesn't that everything "moves" into it and it becomes the owner of those things? But I do understand how `conn` can never move since `tx` has a reference to it. – Paul A Jungwirth Feb 10 '17 at 01:50
  • @PaulAJungwirth your struct 100% has references in it, they just happen to be inside another struct. See the above definition of `Transaction` — it contains a reference. Therefore your struct, which contains `Transaction`, has a reference. – Shepmaster Feb 10 '17 at 01:54
  • Can you elaborate on this?: "If you want to transfer ownership, you just do that." How do you just do it? – Paul A Jungwirth Feb 10 '17 at 01:59
  • @PaulAJungwirth you have to *have* ownership first; something like a `Box` instead of a `&T`, a `Vec` instead of `&[T]` or a `String` instead of a `&str`. That's basically the entire thesis in [Return local String as a slice](http://stackoverflow.com/a/29429698/155423). Then you return it. – Shepmaster Feb 10 '17 at 02:02
1

Using the other answer, I put together working code that lets me bundle up the transaction and all the prepared statements, and pass them around together:

extern crate postgres;

use postgres::{Connection, TlsMode};
use postgres::transaction::Transaction;
use postgres::stmt::Statement;

pub struct Db<'a> {
    tx: Transaction<'a>,
    insert_user: Statement<'a>,
}

fn make_db(conn: &Connection) -> Db {
    let tx = conn.transaction().unwrap();
    let insert_user = tx.prepare("INSERT INTO users VALUES ($1)").unwrap();
    Db {
        tx: tx,
        insert_user: insert_user,
    }
}

pub fn main() {
    let conn = Connection::connect("postgres://paul@localhost/t", TlsMode::None).unwrap();
    let db = make_db(&conn);
    for u in &["foo", "bar"] {
        db.insert_user.execute(&[&u]);
    }
    db.tx.commit().unwrap();
}

As I understand it, Rust wants to guarantee that conn lives as long as db, so by keeping conn outside of the "constructor", the lexical structure ensures that it won't get removed too early.

My struct still doesn't encapsulate conn, which seems too bad to me, but at least it lets me keep everything else together.

Community
  • 1
  • 1
Paul A Jungwirth
  • 23,504
  • 14
  • 74
  • 93