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::{connection::ConnectionOwned, filter, table::Table, Error};
11
12use chuchi_postgres_derive::{row, FromRow};
13use tracing::debug;
14use types::time::DateTime;
15
16#[derive(Debug, FromRow)]
17pub struct ExecutedMigration {
18	datetime: DateTime,
19}
20
21/// Holds all migrations
22///
23/// and checks which migrations already ran, and runs the others
24#[derive(Debug, Clone)]
25pub struct Migrations {
26	table: Table,
27}
28
29impl Migrations {
30	/// Create a new Migrations
31	pub(super) fn new(table_name: Option<String>) -> Self {
32		Self {
33			table: Table::new(
34				table_name.map(Cow::Owned).unwrap_or("migrations".into()),
35			),
36		}
37	}
38
39	pub(super) async fn init(
40		&self,
41		db: &mut ConnectionOwned,
42	) -> Result<(), Error> {
43		let db = db.transaction().await?;
44		let conn = db.connection();
45
46		// replace migrations with the correct table name
47		let table_exists =
48			TABLE_EXISTS.replace("migrations", self.table.name());
49
50		// check if the migrations table exists
51		let [result] =
52			conn.query_one::<[bool; 1], _>(&table_exists, &[]).await?;
53
54		if !result {
55			let create_table =
56				CREATE_TABLE.replace("migrations", self.table.name());
57			conn.batch_execute(&create_table).await?;
58		}
59
60		db.commit().await?;
61
62		Ok(())
63	}
64
65	pub async fn add(
66		&self,
67		conn: &mut ConnectionOwned,
68		name: &str,
69		sql: &str,
70	) -> Result<(), Error> {
71		let trans = conn.transaction().await?;
72		let conn = trans.connection();
73		let table = self.table.with_conn(conn);
74
75		// check if the migration was already executed
76		let existing: Option<ExecutedMigration> =
77			table.select_opt(filter!(&name)).await?;
78		if let Some(mig) = existing {
79			debug!("migration {} was executed at {}", name, mig.datetime);
80			return Ok(());
81		}
82
83		// else execute it
84		conn.batch_execute(&sql).await?;
85
86		table
87			.insert(row! {
88				name,
89				"datetime": DateTime::now(),
90			})
91			.await?;
92
93		trans.commit().await?;
94
95		Ok(())
96	}
97}
98
99const TABLE_EXISTS: &str = "\
100SELECT EXISTS (
101	SELECT FROM information_schema.tables
102	WHERE table_schema = 'public'
103	AND table_name = 'migrations'
104);";
105
106const CREATE_TABLE: &str = "\
107CREATE TABLE migrations (
108    name text PRIMARY KEY,
109    datetime timestamp
110);
111
112CREATE INDEX ON migrations (datetime);";