Closed
Description
A statement is currently modeled as a type that borrows the connection and has methods defined to query/execute it. This is conceptually nice, but has a few issues in practice:
- Since it borrows the connection, it's hard to save off. We have a
prepare_cached
method, but if that doesn't work for your use case you have to write unsafe code to package the statement alongside its connection. - It forces all methods to take
&self
and connection to have an internalRefCell
. This means that we have to dynamically check that the connection/transaction you're using is the "active" one.
An alternate approach is for statement to be a "token" which does not borrow the connection. It has no methods defined on itself, but is instead provided to methods on the connection for use. It holds onto a channel sender and on drop enqueues itself for cleanup. The connection periodically checks the cleanup list and closes all dead statements. This is the approach taken in tokio-postgres.
A high level API sketch:
pub struct Connection { ... }
impl Connection {
pub fn prepare(&mut self, query: &str) -> Result<Statement> { ... }
pub fn query(&mut self, statement: &Statement, params: &[&ToSql]) -> Result<Rows> { ... }
pub fn execute(&mut self, statement: &mut Statement, params: &[&ToSql]) -> Result<u64> { ... }
pub fn query_once(&mut self, query: &str, params: &[&ToSql]) -> Result<Rows> { ... }
pub fn execute_once(&mut self, query: &str, params: &[&ToSql]) -> Result<Rows> { ... }
pub fn transaction<'a>(&'a mut self) -> Result<Transaction<'a>> { ... }
}
pub struct Statement { ... }
pub struct Transaction<'a> { ... }
impl<'a> Transaction<'a> {
// same methods as Connection
}
- Pros
- We no longer need a
RefCell
inside ofConnection
. - You're statically prevented from using a non-active transaction by the borrow checker.
prepare_cached
can go away since you can save off statements how you see fit. r2d2-postgres will need to have some logic to allow you to attach statements to the pooled connection probably.- Slightly less network traffic since we'll delay statement closing until the next use of the connection after drop.
- We no longer need a
- Cons
- We introduce the possibility of people trying to use a statement with the wrong connection, particularly when there's a connection pool involved. We can either panic immediately or just ensure that statement names are globally unique and let the DB reject it.
- The "simple"
Connection::query
andConnection::execute
methods get a bit longer. We could overload a single set of methods with a trait bound that takes both query strings and statements?
fn query<T>(&mut self, statement: &T, params: &[&ToSql]) -> Result<Rows> where T: Query { ... } pub trait Query: Sealed {} impl Query for str {} impl Query for Statement {}
Metadata
Metadata
Assignees
Labels
No labels