schemer_postgres/
lib.rs

1//! An adapter enabling use of the schemer schema migration library with
2//! PostgreSQL.
3//!
4//! # Examples:
5//!
6//! ```rust
7//! extern crate postgres;
8//! #[macro_use]
9//! extern crate schemer;
10//! extern crate schemer_postgres;
11//! extern crate uuid;
12//!
13//! use std::collections::HashSet;
14//!
15//! use postgres::{Client, NoTls, Transaction};
16//! use schemer::{Migration, Migrator};
17//! use schemer_postgres::{PostgresAdapter, PostgresAdapterError, PostgresMigration};
18//! use uuid::Uuid;
19//!
20//! struct MyExampleMigration;
21//! migration!(
22//!     MyExampleMigration,
23//!     "4885e8ab-dafa-4d76-a565-2dee8b04ef60",
24//!     [],
25//!     "An example migration without dependencies.");
26//!
27//! impl PostgresMigration for MyExampleMigration {
28//!     fn up(&self, transaction: &mut Transaction) -> Result<(), PostgresAdapterError> {
29//!         transaction.execute("CREATE TABLE my_example (id integer PRIMARY KEY);", &[])?;
30//!         Ok(())
31//!     }
32//!
33//!     fn down(&self, transaction: &mut Transaction) -> Result<(), PostgresAdapterError> {
34//!         transaction.execute("DROP TABLE my_example;", &[])?;
35//!         Ok(())
36//!     }
37//! }
38//!
39//! fn main() {
40//!     let mut conn = Client::connect(
41//!         "postgresql://postgres@localhost",
42//!         NoTls).unwrap();
43//!     conn.execute("SET search_path = pg_temp", &[]).unwrap();
44//!     let adapter = PostgresAdapter::new(&mut conn, None);
45//!
46//!     let mut migrator = Migrator::new(adapter);
47//!
48//!     let migration = Box::new(MyExampleMigration {});
49//!     migrator.register(migration);
50//!     migrator.up(None);
51//! }
52//! ```
53#![warn(clippy::all)]
54#![forbid(unsafe_code)]
55
56use std::collections::HashSet;
57
58use postgres::{Client, Error as PostgresError, Transaction};
59use uuid::Uuid;
60
61use schemer::{Adapter, Migration};
62
63/// PostgreSQL-specific trait for schema migrations.
64pub trait PostgresMigration: Migration {
65    /// Apply a migration to the database using a transaction.
66    fn up(&self, _transaction: &mut Transaction<'_>) -> Result<(), PostgresError> {
67        Ok(())
68    }
69
70    /// Revert a migration to the database using a transaction.
71    fn down(&self, _transaction: &mut Transaction<'_>) -> Result<(), PostgresError> {
72        Ok(())
73    }
74}
75
76pub type PostgresAdapterError = PostgresError;
77
78/// Adapter between schemer and PostgreSQL.
79pub struct PostgresAdapter<'a> {
80    conn: &'a mut Client,
81    migration_metadata_table: String,
82}
83
84impl<'a> PostgresAdapter<'a> {
85    /// Construct a PostgreSQL schemer adapter.
86    ///
87    /// `table_name` specifies the name of the table that schemer will use
88    /// for storing metadata about applied migrations. If `None`, a default
89    /// will be used.
90    ///
91    /// ```rust
92    /// # extern crate postgres;
93    /// # extern crate schemer_postgres;
94    /// #
95    /// # fn main() {
96    /// let mut conn = postgres::Client::connect(
97    ///     "postgresql://postgres@localhost",
98    ///     postgres::NoTls).unwrap();
99    /// let adapter = schemer_postgres::PostgresAdapter::new(&mut conn, None);
100    /// # }
101    /// ```
102    pub fn new(conn: &'a mut Client, table_name: Option<String>) -> PostgresAdapter<'a> {
103        PostgresAdapter {
104            conn,
105            migration_metadata_table: table_name.unwrap_or_else(|| "_schemer".into()),
106        }
107    }
108
109    /// Initialize the schemer metadata schema. This must be called before
110    /// using `Migrator` with this adapter. This is safe to call multiple times.
111    pub fn init(&mut self) -> Result<(), PostgresError> {
112        self.conn.execute(
113            format!(
114                r#"
115                    CREATE TABLE IF NOT EXISTS {} (
116                        id uuid PRIMARY KEY
117                    ) WITH (
118                        OIDS=FALSE
119                    )
120                "#,
121                self.migration_metadata_table
122            )
123            .as_str(),
124            &[],
125        )?;
126        Ok(())
127    }
128}
129
130impl<'a> Adapter for PostgresAdapter<'a> {
131    type MigrationType = dyn PostgresMigration;
132
133    type Error = PostgresAdapterError;
134
135    fn applied_migrations(&mut self) -> Result<HashSet<Uuid>, Self::Error> {
136        let rows = self.conn.query(
137            format!("SELECT id FROM {};", self.migration_metadata_table).as_str(),
138            &[],
139        )?;
140        Ok(rows.iter().map(|row| row.get(0)).collect())
141    }
142
143    fn apply_migration(&mut self, migration: &Self::MigrationType) -> Result<(), Self::Error> {
144        let mut trans = self.conn.transaction()?;
145        migration.up(&mut trans)?;
146        trans.execute(
147            format!(
148                "INSERT INTO {} (id) VALUES ($1::uuid);",
149                self.migration_metadata_table
150            )
151            .as_str(),
152            &[&migration.id()],
153        )?;
154        trans.commit()
155    }
156
157    fn revert_migration(&mut self, migration: &Self::MigrationType) -> Result<(), Self::Error> {
158        let mut trans = self.conn.transaction()?;
159        migration.down(&mut trans)?;
160        trans.execute(
161            format!(
162                "DELETE FROM {} WHERE id = $1::uuid;",
163                self.migration_metadata_table
164            )
165            .as_str(),
166            &[&migration.id()],
167        )?;
168        trans.commit()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use postgres::NoTls;
176    use schemer::test_schemer_adapter;
177    use schemer::testing::*;
178
179    impl PostgresMigration for TestMigration {}
180
181    impl<'a> TestAdapter for PostgresAdapter<'a> {
182        fn mock(id: Uuid, dependencies: HashSet<Uuid>) -> Box<Self::MigrationType> {
183            Box::new(TestMigration::new(id, dependencies))
184        }
185    }
186
187    fn build_test_connection() -> Client {
188        let mut client = Client::connect("postgresql://postgres@localhost", NoTls).unwrap();
189        client.execute("SET search_path = pg_temp", &[]).unwrap();
190        client
191    }
192
193    fn build_test_adapter(conn: &mut Client) -> PostgresAdapter<'_> {
194        let mut adapter = PostgresAdapter::new(conn, None);
195        adapter.init().unwrap();
196        adapter
197    }
198
199    test_schemer_adapter!(
200        let mut conn = build_test_connection(),
201        build_test_adapter(&mut conn));
202}