Self referencing struct (but not directly)

I am using duckdb, which when obtaining data, has following types:

connection
statement<'connection>
rows<'statement>

I'd like to store all of that together in one struct to have some sort of type I can pass around.
This is obviously not supported by Rust. What's the best solution in such case?

I looked at ouroboros, but it seems this is not a usecase it supports.

I'm not sure what you mean by "but not directly", but this is in fact what is generally meant by "self-referencial struct" (Statement<'conn> contains a &'conn Connection and Rows<'stmt>/Row<'stmt> contain a &'stmt Statement<'stmt>).

So it is the usecase ouroboros and yoke and the like are aimed at, but I'm afraid I won't be of help when it comes to actually getting things to compile with those crates.

The primary solution is to use separate variables/parameters.

2 Likes

No matter how appealing "storing all of these together" might be, you really don't want to do it. Prepared statements and results from executing a query are temporary intermediate types. You almost always want to extract owned values from the query result and store those instead.

There is a very important warning in the docs which should be noted: Row in duckdb - Rust

This ValueRef is valid only as long as this Row, which is enforced by it’s lifetime. This means that while this method is completely safe, it can be somewhat difficult to use, and most callers will be better served by get or get_unwrap.

(Emphasis my own.)

And ... yeah, that looks familiar, where have I seen this before? Oh yeah, it's the same phrasing used in Row in rusqlite - Rust!

It is of course quite nice that you are allowed to access values in your in-memory database through just a simple pointer indirection, but actually doing so in practice is rare if you want readable code (e.g. not a 5,000 line function with all of your business logic inlined). You probably don't need to do that anyway. Go ahead and clone the data into your structs, it will be ok.

2 Likes

I do. I just don't want to do that in the function that creates connection.
So I could consider this to be a control flow issue rather then memory management, but still, even for the sake of learning, I'd like to know how to do it (safely).

1 Like

You can indeed make a type that contains

using ouroboros, self_cell or yoke. All of them do require using two structs however since there are two different parents/owners (statement borrows from connection while rows borrows from statement).

Cargo.toml:

[dependencies]
ouroboros = "0.18.4"
self_cell = "1.1.0"
yoke = { version = "=0.7.4", features = ["derive"] }

lib.rs:

struct Connection(());

struct Statement<'conn>(&'conn Connection);

struct Rows<'stmt>(&'stmt Statement<'stmt>);

#[ouroboros::self_referencing]
struct OuroborosOwnedStatement {
    connection: Connection,
    #[borrows(connection)]
    #[covariant] // OR  #[not_covariant]
    statement: Statement<'this>,
}
#[ouroboros::self_referencing]
struct OuroborosOwnedRows {
    statement: OuroborosOwnedStatement,
    #[borrows(statement)]
    #[covariant] // OR  #[not_covariant]
    rows: Rows<'this>,
}

impl OuroborosOwnedStatement {
    pub fn create() -> Self {
        Self::new(Connection(()), |conn| Statement(conn))
    }
}
impl OuroborosOwnedRows {
    pub fn create() -> Self {
        Self::new(OuroborosOwnedStatement::create(), |statement| {
            Rows(statement.borrow_statement())
        })
    }
}

self_cell::self_cell!(
    struct SelfCellOwnedStatement {
        owner: Connection,
        #[covariant] // OR  #[not_covariant]
        dependent: Statement,
    }
);
self_cell::self_cell!(
    struct SelfCellOwnedRows {
        owner: SelfCellOwnedStatement,
        #[covariant] // OR  #[not_covariant]
        dependent: Rows,
    }
);

impl SelfCellOwnedStatement {
    pub fn create() -> Self {
        Self::new(Connection(()), |conn| Statement(conn))
    }
}
impl SelfCellOwnedRows {
    pub fn create() -> Self {
        Self::new(SelfCellOwnedStatement::create(), |statement| {
            Rows(statement.borrow_dependent())
        })
    }
}

// See derive examples at: https://github.com/unicode-org/icu4x/blob/e7c271ad2dcc75416d86cdddeb9664db2ba7d97d/utils/yoke/derive/examples/yoke_derive.rs

/// Newtype that implements `Yokeable` for `Statement`.
#[derive(yoke::Yokeable)]
struct YokeableStatement<'conn>(Statement<'conn>);
#[derive(yoke::Yokeable)]
struct YokeableRows<'statement>(Rows<'statement>);

struct YokeOwnedStatement(yoke::Yoke<YokeableStatement<'static>, Box<Connection>>);
impl YokeOwnedStatement {
    pub fn create() -> Self {
        let conn = Box::new(Connection(()));
        Self(yoke::Yoke::attach_to_cart(conn, |conn| {
            YokeableStatement(Statement(conn))
        }))
    }
}

struct YokeOwnedRows(yoke::Yoke<YokeableRows<'static>, Box<YokeOwnedStatement>>);
impl YokeOwnedRows {
    pub fn create() -> Self {
        let statement = Box::new(YokeOwnedStatement::create());
        Self(yoke::Yoke::attach_to_cart(statement, |statement| {
            YokeableRows(Rows(&statement.0.get().0))
        }))
    }
}

3 Likes

That's fantastic example, thank you. I had to modify it slightly to handle mutability, but it works now as I'd like it to.