1

I wanted to design a "database" interface that encapsulates tables. One can work with tables directly, so the table object needs to reference the connection pool owned by the database object. Here's a sketch of what I mean:

struct Db<'a> {
    pool: Pool,
    tables: HashMap<String, Table<'a>>,
}

struct Table<'a> {
    name: String,
    pool: &'a Pool,
}

impl<'a> Db<'a> {
    fn new(dsn: &str, tables: &[&str]) -> Self {
        let pool = Pool::new(isn);
        Db {
            pool: pool,
            tables: tables
                .iter()
                .map(|t| {
                    (
                        t.to_string(),
                        Table {
                            name: t.to_string(),
                            pool: &pool,
                        },
                    )
                })
                .collect(),
        }
    }

    fn table(&self, table: &str) -> &Table {
        self.tables.get(table).unwrap()
    }
}

This of course fails because the compiler doesn't know that pool will be referenced by the the same Db object as the tables field is a part of. How can I convince the compiler that the references to pool in the tables HashMap will last just as long as the pool itself?


FWIW, I tried adding a method to Db that operates on a mutable &self with a mutable hash map, like so:

impl<'a> Db<'a> {
    fn new(dsn: &str, tables: &[&str]) -> Self {
        let mut ret = Db {
            pool: Pool::new(dsn),
            tables: HashMap::new(),
        };
        ret.mk_tables(tables);
        ret
    }

    fn mk_tables(&mut self, tables: &[&str]) {
        for t in tables {
            self.tables.insert(
                t.to_string(),
                Table {
                    name: t.to_string(),
                    pool: &self.pool,
                },
            );
        }
    }

    fn table(&self, table: &str) -> &Table {
        self.tables.get(table).unwrap()
    }
}

I figured this way the compiler would know they were part of the same object, but apparently not:

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  --> src/main.rs:40:27
   |
40 |                     pool: &self.pool,
   |                           ^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the method body at 34:5...
  --> src/main.rs:34:5
   |
34 |     fn mk_tables(&mut self, tables: &[&str]) {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:40:27
   |
40 |                     pool: &self.pool,
   |                           ^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the impl at 16:6...
  --> src/main.rs:16:6
   |
16 | impl<'a> Db<'a> {
   |      ^^
note: ...so that the expression is assignable
  --> src/main.rs:38:17
   |
38 | /                 Table {
39 | |                     name: t.to_string(),
40 | |                     pool: &self.pool,
41 | |                 },
   | |_________________^
   = note: expected `Table<'a>`
              found `Table<'_>`

I get that the pool outlives the implicit lifetime of the mut &self in the method signature, but I'm at a loss to determine how to work around it. Somehow reference the 'a from the implementation declaration?

theory
  • 9,178
  • 10
  • 59
  • 129
  • 1
    Does this answer your question? [Why can't I store a value and a reference to that value in the same struct?](https://stackoverflow.com/q/32300132/2766908) – pretzelhammer Feb 14 '21 at 19:59
  • Maybe. I didn't realize that returning a value (struct) from a function (constructor) moves the object to a new address! It makes sense that if it's moved, a reference to it would also have to move, which of course doesn't happen. I've used patterns like this in other languages, so assumed there must be a way, but the best I've come up with is to store the table names in a HashSet and to return (and move) a new Table from the `table()` method every time. It works, but seems silly for a method that will construct the same structure every time it's called. Is there no way to memoize it? – theory Feb 14 '21 at 21:19
  • It's a common point of confusion; the linked question is the #1 most frequently asked [tag:rust] question on SO. The gist of it is that you can only do this when the struct is statically guaranteed to never move. – trent Feb 14 '21 at 21:20
  • 1
    In most other languages references are garbage collected, so one way to approach it is to wrap `Pool` in `Rc` or `Arc` (as the linked question also mentions): [example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a3c1f89c3028b72429acb794a421cee3) This does give up some of the raw performance of plain references, but you have to sacrifice something. – trent Feb 14 '21 at 21:30
  • Ah-hah, yes I see. So I guess the question is whether there is more benefit to indroducing references counting and its overhead vs. having `table()` return a new table object every time. – theory Feb 14 '21 at 21:35
  • `Rc` seems to be the answer I was looking for, thank you! – theory Feb 14 '21 at 21:43

0 Answers0