rustlavel_db/driver.rs
1//! The contract every database driver implements.
2//!
3//! Above this line the framework is one codebase: the query builder, the schema
4//! builder, the migrator and the ORM are written once. Below it sits a wire
5//! protocol per database, each written from scratch.
6//!
7//! Both traits hand back boxed futures rather than using `async fn`, because a
8//! trait with `async fn` is not object-safe, and the pool has to hold a driver
9//! whose type it does not know.
10
11use crate::dialect::Dialect;
12use crate::row::Row;
13use crate::value::Value;
14use rustlavel_core::Result;
15use std::pin::Pin;
16use std::sync::Arc;
17
18/// A future returned from a trait method.
19pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
20
21/// What a statement returned.
22#[derive(Debug, Default)]
23pub struct QueryResult {
24 pub rows: Vec<Row>,
25 /// Rows affected, as the database reported it.
26 pub affected: u64,
27 /// The key an insert generated, when the database volunteered one.
28 ///
29 /// PostgreSQL and SQL Server return it as a row; MySQL reports it in the
30 /// packet that acknowledges the insert, with no row at all — which is why
31 /// this is separate from `rows`.
32 pub last_insert_id: Option<i64>,
33}
34
35/// One physical connection.
36pub trait DriverConnection: Send {
37 /// Run a statement with bound parameters.
38 ///
39 /// Parameters never enter the SQL text, whatever the database: that is what
40 /// makes injection structurally impossible rather than a matter of
41 /// remembering to escape.
42 fn query<'a>(
43 &'a mut self,
44 sql: &'a str,
45 params: &'a [Value],
46 ) -> BoxFuture<'a, Result<QueryResult>>;
47
48 /// Run a statement with no parameters — DDL, and transaction control.
49 fn simple_query<'a>(&'a mut self, sql: &'a str) -> BoxFuture<'a, Result<QueryResult>>;
50
51 /// Whether this connection is known to be unusable, so the pool discards it
52 /// instead of handing it to the next caller.
53 fn is_broken(&self) -> bool;
54
55 /// Whether a transaction is still open. A connection left inside one must
56 /// never go back into rotation.
57 fn in_transaction(&self) -> bool;
58
59 /// Say goodbye politely, then hang up.
60 fn close(self: Box<Self>) -> BoxFuture<'static, ()>;
61}
62
63/// Opens connections, and knows what dialect they speak.
64pub trait Driver: Send + Sync + 'static {
65 fn dialect(&self) -> Arc<dyn Dialect>;
66
67 fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>>;
68
69 /// How this connection is described in an error or a log line.
70 ///
71 /// Must never contain the password — it ends up in messages people paste
72 /// into issues.
73 fn describe(&self) -> String;
74
75 /// How many connections the pool should allow at once.
76 fn max_connections(&self) -> usize {
77 10
78 }
79
80 /// Which generation of credentials a connection opened now would belong to.
81 ///
82 /// Zero when the driver's credentials never change, which is the common
83 /// case and the one that costs nothing: the pool compares the number it
84 /// stored against this, and zero always equals zero.
85 fn generation(&self) -> u64 {
86 0
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::dialect::Postgres;
94 use rustlavel_core::Error;
95
96 /// A driver that never connects, to prove the traits compose without a
97 /// database anywhere near them.
98 struct Offline;
99
100 impl Driver for Offline {
101 fn dialect(&self) -> Arc<dyn Dialect> {
102 Arc::new(Postgres)
103 }
104
105 fn connect(&self) -> BoxFuture<'_, Result<Box<dyn DriverConnection>>> {
106 Box::pin(async { Err(Error::msg("this driver never connects")) })
107 }
108
109 fn describe(&self) -> String {
110 "offline://nowhere".into()
111 }
112 }
113
114 #[tokio::test]
115 async fn a_driver_can_be_held_without_naming_its_type() {
116 let driver: Arc<dyn Driver> = Arc::new(Offline);
117
118 assert_eq!(driver.dialect().name(), "postgres");
119 assert_eq!(driver.describe(), "offline://nowhere");
120 assert_eq!(driver.max_connections(), 10);
121 assert!(driver.connect().await.is_err());
122 }
123}