1

I am using the rustqlite library for a SQLite database (playground)

use std::ops::Deref;

pub struct Connection {
}

impl Connection {
  pub fn transaction(&mut self) -> Transaction {
    Transaction::new(self)
  }
}

pub struct Transaction<'conn> {
    conn: &'conn Connection
}

impl Transaction<'_> {
    pub fn new(conn: &mut Connection) -> Transaction {
        Transaction{ conn }
    }
    
    pub fn commit(mut self) -> Result<(), ()> {
        Ok(())
    }
}

impl Deref for Transaction<'_> {
    type Target = Connection;

    #[inline]
    fn deref(&self) -> &Connection {
        self.conn
    }
}

With this implementation, the Transaction object will take the ownership of the Connection object. At the same time, it also implements the Deref trait so we can call all methods from the Transaction struct as from the Connection struct.

The implementation detail is here

From my application code, I want to have a single object that can be represented by either Transaction or Connection. This is necessary because the logic has a flag to decide to use transaction or not. There is a cast to treat the Transaction object as the Connection object:

let conn = create_connection(); // Connection
let tx = conn.transaction(); // Transaction
let conn: &Transaction = &tx; // cast back to Connection type from the Transaction type

However, I don't know how to arrange this code from the application POV with the condition. Here is my pseudocode:

pub fn execute(is_tx: bool) {
    // conn will have Connection type
    let conn = match is_tx {
        true => &create_connection(),
        false => {
            let x = create_connection().transaction();
            let t: &Connection = &x;
            t
        }
    };

    // do other things with conn object
}

pub fn create_connection() -> Connection {
    Connection{}
}

However, there will be an error

error[E0716]: temporary value dropped while borrowed
  --> src/lib.rs:36:21
   |
36 |             let x = create_connection().transaction();
   |                     ^^^^^^^^^^^^^^^^^^^              - temporary value is freed at the end of this statement
   |                     |
   |                     creates a temporary which is freed while still in use
37 |             let t: &Connection = &x;
   |                                  -- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

error[E0597]: `x` does not live long enough
  --> src/lib.rs:37:34
   |
37 |             let t: &Connection = &x;
   |                                  ^^ borrowed value does not live long enough
38 |             t
   |             - borrow later used here
39 |         }
   |         - `x` dropped here while still borrowed

I understand the error, but I've tried a couple of workarounds without success, mostly because the Transaction struct takes ownership of the Connection struct. How can I fix this?

Trần Kim Dự
  • 5,872
  • 12
  • 55
  • 107
  • You have two options, you can return a union type, or a Box. – Aron May 12 '21 at 17:40
  • *I don't really post an error because I understand what it is* - the point is that you post an error so that **other people** can (a) find this question when they have it and (b) ensure that it's the same problem when they try to solve it for you. – Shepmaster May 12 '21 at 17:53
  • @Aron saying "union type" is misleading here, as it's *very unlikely* that people want to use the thing Rust calls a `union`. – Shepmaster May 12 '21 at 17:55
  • I believe your question is answered by [How can I conditionally provide a default reference without performing unnecessary computation when it isn't used?](https://stackoverflow.com/q/58559969/155423). – Shepmaster May 12 '21 at 17:59
  • I just read the question. Then no, it's not. – Trần Kim Dự May 12 '21 at 18:02
  • Your example code never uses `transaction`, so it seems like you could delete that. – Shepmaster May 12 '21 at 18:02
  • It did use. Can you please take a look again. `let x = create_connection().transaction();` – Trần Kim Dự May 12 '21 at 18:09
  • *"the `Transaction` object will take the ownership of the `Connection` object"*, no it doesn't it only holds a *reference* to the connection, meaning `create_connection().transaction()` is almost immediately wrong since the result of `create_connection()` will be destroyed at the end of the statement. You have similar reference problems in the `execute()` match block. @Aron mentioned using a Boxed trait, which is the right way to go for this kind of polymorphism, but `Deref` is ill suited for this task. – kmdreko May 12 '21 at 19:04
  • @kmdreko As I explained in the question, that is the library code, which I don't own. – Trần Kim Dự May 13 '21 at 00:25
  • @Shepmaster, appologies, I've been using Typescript too much lately. The Rust term I was looking for was Enum types. We have in effect `Either` and then `impl Deref`. – Aron May 13 '21 at 02:33
  • What is the `WrappedConnection` here? And can you help me to show the code. I am happy to learn from this. Thanks – Trần Kim Dự May 13 '21 at 03:27
  • Thanks for @Shepmaster for helping me a lot in refining the question. – Trần Kim Dự May 13 '21 at 11:37

1 Answers1

2

Disclaimer: I haven't tried this code. But it is only here to give you an idea of the direction to go in.

Firstly, local variables (on the stack) in Rust must be fixed sized. This is one of the problems you are facing. Transaction and Connection aren't the same sized. So you can't achieve "Polymorphism" on the stack, without some tricks.

The two ways to do this are Enum types, and Boxing (putting the structs on the Heap, and adding a VTable).

I won't go over Boxing, since that is relatively simple.

The second problem you have is that Transaction's lifetime is tied to the Connection, so any moving of the Transaction will require you to move the Connection as well.

enum MyConnection<'a> {
   TransactionConnection {
       transaction: Transaction<'a>
    },
   NakedConnection{
    connection: Connection
   }
}

impl MyConnection<'a> {
    fn commit(mut &self) -> Result<()> {
        match self {
           MyConnection::NakedConnection =>
              Ok(()),
           MyConnection::TransactionConnection { transaction } =>
              transaction.commit()
        }
    }
}

impl<'a> Deref for MyConnection<'a>
{
    type Target = Connection;
    #[inline]
    fn deref(&self) -> &Connection {
        match self {
            MyConnection::TransactionConnection { transaction } =>
                transaction.conn,
            MyConnection::NakedConnection { connection } =>
                connection,
        }
    }
}

These enums and the Deref will allow you to hold a struct that can access the connection.

This is how you use the above code.

pub fn execute(is_tx: bool) {
    // conn will have Connection type
    let mut conn = create_connection();
    let conn = match is_tx {
        false => {
            MyConnection::NakedConnection { connection: conn }
        },
        true => {
            let trans = conn.transaction();
            MyConnection::TransactionConnection {                
                transaction: trans,
            }
        }
    };
    conn.do_stuff();
    conn.commit();
}

Notice that the create_connection has been move outside of the match. This is so that the scope of the connection will always be greater than the scope 'a of MyConnection. This "solves" the second problem.

Aron
  • 15,464
  • 3
  • 31
  • 64
  • Thanks so much. I just edited your answer in the `Deref` function: return directly the transaction (because I cannot get the connection from the transaction due to the library limitation). My question is: Why this cast works, but not in my below code? ``` let conn = match is_tx { true => { self.get_mut_conn() } false => { self.get_mut_conn().transaction()? } }; ``` The error is: expected `&mut Connection`, found struct `Transaction` – Trần Kim Dự May 13 '21 at 04:45
  • Other question is: How can I call the commit on the transaction object, which is method signature is `pub fn commit(mut self) -> Result<()>`. When I use the same logic to commit, I have the following error: "cannot move out of `*transaction` which is behind a shared reference" – Trần Kim Dự May 13 '21 at 04:53
  • Your edit is broken. `fn deref(&self) -> &Connection` says "I promise to return a reference to a Connection", then in the arm of the match, you break that promise by returning a `Transaction`. – Aron May 13 '21 at 04:54
  • you mean the edit in your original answer? I think it worked (it already compiled and run correctly). – Trần Kim Dự May 13 '21 at 05:02
  • And if you mean the code in my comment section, then yes, I agree what you said. The thing I don't know is: With Deref, Rust allows to cast like this (as in your example), but I don't know why in my example, it doesn't allow. – Trần Kim Dự May 13 '21 at 05:08
  • Rust doesn't really have "casting". At least not like in Java or C#. If you want casting like symmantics you need to use `Box`. Which means you want a fat pointer(with VTable) to the heap. – Aron May 13 '21 at 05:18
  • I might be use the wrong word here. The thing I want to say is this line is valid in Rust right? `let t :&Connection = &transaction;` (because Transaction implements Deref) – Trần Kim Dự May 13 '21 at 05:23
  • Ah. I see the confusion now. That line is not casting, it is "taking a reference". It is syntactic sugar that means `let conn= conn.deref()`. A reference, is like a pointer, but safe. This syntactic sugar is typically used to build smart pointers like `Box`. I'm using it this way, so you can treat the first `conn` like it was a `&Connection`. – Aron May 13 '21 at 05:35
  • Thanks for explanation. My question is: I use a same approach for the following code ```let conn = match is_tx { true => { self.get_mut_conn() } false => { self.get_mut_conn().transaction()? } }; ``` As you can see in my original question, but the error is "The error is: expected &mut Connection, found struct Transaction". Why it doesn't work as your solution? – Trần Kim Dự May 13 '21 at 05:41
  • You are missing the MyConnection Enum. You need that. You also need to move the `get_mut_conn` outside the `match` block due to lifetime issues. – Aron May 13 '21 at 05:55
  • You might want to make another question for the different aspects of refactoring the code for your usage. – Aron May 13 '21 at 05:59
  • Thanks. So I think the only problem that I met because of the lifetime issue right? By using an enum for this purpose, I can retain the object. I think this could be possibly the reason I cannot resolve the error. – Trần Kim Dự May 13 '21 at 06:08
  • The second problem you have is that Transaction's lifetime is tied to the Connection, so any moving of the Transaction will require you to move the Connection as well. --> you mentioned about this. So the way we solve is using Enum right? – Trần Kim Dự May 13 '21 at 06:19
  • No. The Enum is to solve the "poly morphism" problem. If you are staying on the stack, you need a memory structure that is of fixed size to represent either "just a connection" or "a transaction". Moving the initialization of the `conn` outside, means we ensure that the connection's lifetime is longer than the transaction. – Aron May 13 '21 at 06:30