chuchi_postgres/
migrations.rs

1//! Migrations
2//!
3//! How do migrations work
4//!
5//! A migration is an sql script which can be executed on the database
6//! this script is only executed once and then stored in the database.
7
8use std::borrow::Cow;
9
10use crate::{
11	Error,
12	connection::{Connection, ConnectionOwned},
13	filter,
14	table::Table,
15};
16
17use chuchi_postgres_derive::{FromRow, row};
18use tracing::debug;
19use types::time::DateTime;
20
21#[derive(Debug, FromRow)]
22pub struct ExecutedMigration {
23	datetime: DateTime,
24}
25
26/// Holds all migrations
27///
28/// and checks which migrations already ran, and runs the others
29#[derive(Debug, Clone)]
30pub struct Migrations {
31	table: Table,
32}
33
34impl Migrations {
35	/// Create a new Migrations
36	pub(super) fn new(table_name: Option<String>) -> Self {
37		Self {
38			table: Table::new(
39				table_name.map(Cow::Owned).unwrap_or("migrations".into()),
40			),
41		}
42	}
43
44	pub(super) async fn init(
45		&self,
46		db: &mut ConnectionOwned,
47	) -> Result<(), Error> {
48		let db = db.transaction().await?;
49		let conn = db.connection();
50
51		// replace migrations with the correct table name
52		let table_exists =
53			TABLE_EXISTS.replace("migrations", self.table.name());
54
55		// check if the migrations table exists
56		let [result] =
57			conn.query_one::<[bool; 1], _>(&table_exists, &[]).await?;
58
59		if !result {
60			let create_table =
61				CREATE_TABLE.replace("migrations", self.table.name());
62			conn.batch_execute(&create_table).await?;
63		}
64
65		db.commit().await?;
66
67		Ok(())
68	}
69
70	pub async fn add(
71		&self,
72		conn: &mut ConnectionOwned,
73		name: &str,
74		sql: &str,
75	) -> Result<(), Error> {
76		let trans = conn.transaction().await?;
77		let conn = trans.connection();
78
79		// check if the migration was already executed
80		let executed = self.get(conn, name).await?;
81		if let Some(mig) = executed {
82			debug!("migration {} was executed at {}", name, mig.datetime);
83			return Ok(());
84		}
85
86		// else execute it
87		conn.batch_execute(&sql).await?;
88
89		self.set(conn, name).await?;
90
91		trans.commit().await?;
92
93		Ok(())
94	}
95
96	pub async fn get(
97		&self,
98		conn: Connection<'_>,
99		name: &str,
100	) -> Result<Option<ExecutedMigration>, Error> {
101		let table = self.table.with_conn(conn);
102
103		// check if the migration was already executed
104		table.select_opt(filter!(&name)).await
105	}
106
107	pub async fn set(
108		&self,
109		conn: Connection<'_>,
110		name: &str,
111	) -> Result<(), Error> {
112		let table = self.table.with_conn(conn);
113
114		table
115			.insert(row! {
116				name,
117				"datetime": DateTime::now(),
118			})
119			.await?;
120
121		Ok(())
122	}
123}
124
125const TABLE_EXISTS: &str = "\
126SELECT EXISTS (
127	SELECT FROM information_schema.tables
128	WHERE table_schema = 'public'
129	AND table_name = 'migrations'
130);";
131
132const CREATE_TABLE: &str = "\
133CREATE TABLE migrations (
134    name text PRIMARY KEY,
135    datetime timestamp
136);
137
138CREATE INDEX ON migrations (datetime);";