chuchi_postgres/
database.rs

1use deadpool_postgres::{CreatePoolError, Pool, PoolError, Runtime};
2
3use tokio_postgres::Error as PgError;
4use tokio_postgres::NoTls;
5
6pub use deadpool::managed::TimeoutType;
7pub use deadpool_postgres::{Config as PgConfig, ConfigError};
8
9use crate::connection::ConnectionOwned;
10use crate::migrations::Migrations;
11use crate::table::TableOwned;
12use crate::table::TableTemplate;
13
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum DatabaseError {
17	#[error("The database configuration is invalid {0}")]
18	Config(ConfigError),
19
20	#[error("Getting a connection timed out {0:?}")]
21	Timeout(TimeoutType),
22
23	#[error("Connection error {0}")]
24	Connection(#[from] crate::Error),
25
26	#[error("Postgres error {0}")]
27	Other(#[from] PgError),
28}
29
30#[cfg(feature = "chuchi")]
31mod impl_chuchi {
32	use std::error::Error as StdError;
33
34	use super::*;
35	use chuchi::error::{ErrorKind, ServerErrorKind};
36
37	impl chuchi::extractor::ExtractorError for DatabaseError {
38		fn error_kind(&self) -> ErrorKind {
39			ErrorKind::Server(ServerErrorKind::InternalServerError)
40		}
41
42		fn into_std(self) -> Box<dyn StdError + Send + Sync> {
43			Box::new(self)
44		}
45	}
46}
47
48/// Configuration builder
49///
50/// This allows to configure a database.
51///
52/// For full flexibility use `from_pg_config`.
53#[derive(Clone, Debug, Default)]
54pub struct Config {
55	pg_config: PgConfig,
56	migration_table: Option<String>,
57}
58
59impl Config {
60	/// Creates a config using the given `PgConfig`.
61	pub fn from_pg_config(pg_config: PgConfig) -> Self {
62		Self {
63			pg_config,
64			..Default::default()
65		}
66	}
67
68	/// Set's the host.
69	pub fn host(mut self, host: impl Into<String>) -> Self {
70		self.pg_config.host = Some(host.into());
71		self
72	}
73
74	/// Set's the database name.
75	pub fn dbname(mut self, dbname: impl Into<String>) -> Self {
76		self.pg_config.dbname = Some(dbname.into());
77		self
78	}
79
80	/// Set's the user.
81	pub fn user(mut self, user: impl Into<String>) -> Self {
82		self.pg_config.user = Some(user.into());
83		self
84	}
85
86	/// Set's the password.
87	pub fn password(mut self, password: impl Into<String>) -> Self {
88		self.pg_config.password = Some(password.into());
89		self
90	}
91
92	/// Set's the migration table name, by default it is `migrations`.
93	pub fn migration_table(mut self, table: impl Into<String>) -> Self {
94		self.migration_table = Some(table.into());
95		self
96	}
97
98	/// Get's a reference to the `PgConfig`.
99	pub fn pg_config(&self) -> &PgConfig {
100		&self.pg_config
101	}
102
103	/// Get's a mutable reference to the `PgConfig`.
104	pub fn pg_config_mut(&mut self) -> &mut PgConfig {
105		&mut self.pg_config
106	}
107}
108
109/// Database Type
110///
111/// Contains a connection pool and a migration manager
112#[derive(Debug, Clone)]
113pub struct Database {
114	pool: Pool,
115	migrations: Migrations,
116}
117
118impl Database {
119	/// Create a new database
120	pub async fn new(
121		name: impl Into<String>,
122		user: impl Into<String>,
123		password: impl Into<String>,
124	) -> Result<Self, DatabaseError> {
125		Self::with_host("localhost", name, user, password).await
126	}
127
128	/// Create a new database with a host
129	pub async fn with_host(
130		host: impl Into<String>,
131		name: impl Into<String>,
132		user: impl Into<String>,
133		password: impl Into<String>,
134	) -> Result<Self, DatabaseError> {
135		Self::with_cfg(
136			Config::default()
137				.host(host)
138				.dbname(name)
139				.user(user)
140				.password(password),
141		)
142		.await
143	}
144
145	/// Create a new database with a custom configuration.
146	pub async fn with_cfg(cfg: Config) -> Result<Self, DatabaseError> {
147		let pool = cfg
148			.pg_config
149			.create_pool(Some(Runtime::Tokio1), NoTls)
150			.map_err(|e| match e {
151				CreatePoolError::Config(e) => DatabaseError::Config(e),
152				CreatePoolError::Build(_) => unreachable!(
153					"since we provide a runtime this should never happen"
154				),
155			})?;
156
157		let this = Self {
158			pool,
159			migrations: Migrations::new(cfg.migration_table),
160		};
161
162		// just make sure the connection worked
163		let mut db = this.get().await?;
164
165		this.migrations.init(&mut db).await?;
166
167		Ok(this)
168	}
169
170	/// Get a connection from the pool.
171	pub async fn get(&self) -> Result<ConnectionOwned, DatabaseError> {
172		self.pool
173			.get()
174			.await
175			.map_err(|e| match e {
176				PoolError::Timeout(tim) => DatabaseError::Timeout(tim),
177				PoolError::Backend(e) => e.into(),
178				PoolError::Closed => todo!("when can a pool be closed?"),
179				PoolError::NoRuntimeSpecified => unreachable!(),
180				PoolError::PostCreateHook(e) => {
181					todo!("what is this error {e:?}?")
182				}
183			})
184			.map(ConnectionOwned)
185	}
186
187	/// Get the migrations.
188	pub fn migrations(&self) -> Migrations {
189		self.migrations.clone()
190	}
191
192	/// Get a table from the database
193	///
194	/// ## Note
195	/// This might be removed in the next version.
196	pub fn table_owned<T>(&self, name: &'static str) -> TableOwned<T>
197	where
198		T: TableTemplate,
199	{
200		TableOwned::new(self.clone(), name)
201	}
202}