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.