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}