on main help sign in
Add new `custom` action
88fca45fabianlindfors
README.md+28
@@ -33,6 +33,7 @@ Designed for Postgres 12 and later.
33 - [Enums](#enums)
33 - [Enums](#enums)
34 - [Create enum](#create-enum)
34 - [Create enum](#create-enum)
35 - [Remove enum](#remove-enum)
35 - [Remove enum](#remove-enum)
36+ - [Custom](#custom)
36- [Commands and options](#commands-and-options)
37- [Commands and options](#commands-and-options)
37 - [`reshape migrate`](#reshape-migrate)
38 - [`reshape migrate`](#reshape-migrate)
38 - [`reshape complete`](#reshape-complete)
39 - [`reshape complete`](#reshape-complete)
@@ -531,6 +532,33 @@ type = "remove_enum"
531enum = "mood"
532enum = "mood"
532```
533```
533
534
535+### Custom
536+
537+The `custom` action lets you create a migration which runs custom SQL. It should be used with great care as it provides no guarantees of zero-downtime and will simply run whatever SQL is provided. Use other actions whenever possible as they are explicitly designed for zero downtime.
538+
539+There are three optional settings available which all accept SQL queries. All queries need to be idempotent, for example by using `IF NOT EXISTS` wherever available.
540+
541+- `start`: run when a migration is started using `reshape migrate`
542+- `complete`: run when a migration is completed using `reshape complete`
543+- `abort`: run when a migration is aborted using `reshape abort`
544+
545+*Example: enable PostGIS and pg_stat_statements extensions*
546+
547+```toml
548+[[actions]]
549+type = "custom"
550+
551+start = """
552+ CREATE EXTENSION IF NOT EXISTS postgis;
553+ CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
554+"""
555+
556+abort = """
557+ DROP EXTENSION IF NOT EXISTS postgis;
558+ DROP EXTENSION IF NOT EXISTS pg_stat_statements;
559+"""
560+```
561+
534## Commands and options
562## Commands and options
535
563
536### `reshape migrate`
564### `reshape migrate`
src/migrations/custom.rsnew file+61
@@ -0,0 +1,61 @@
1+use super::{Action, MigrationContext};
2+use crate::{
3+ db::{Conn, Transaction},
4+ schema::Schema,
5+};
6+use serde::{Deserialize, Serialize};
7+
8+#[derive(Serialize, Deserialize, Debug)]
9+pub struct Custom {
10+ #[serde(default)]
11+ pub start: Option<String>,
12+
13+ #[serde(default)]
14+ pub complete: Option<String>,
15+
16+ #[serde(default)]
17+ pub abort: Option<String>,
18+}
19+
20+#[typetag::serde(name = "custom")]
21+impl Action for Custom {
22+ fn describe(&self) -> String {
23+ "Running custom migration".to_string()
24+ }
25+
26+ fn run(
27+ &self,
28+ _ctx: &MigrationContext,
29+ db: &mut dyn Conn,
30+ _schema: &Schema,
31+ ) -> anyhow::Result<()> {
32+ if let Some(start_query) = &self.start {
33+ println!("Running query: {}", start_query);
34+ db.run(start_query)?;
35+ }
36+
37+ Ok(())
38+ }
39+
40+ fn complete<'a>(
41+ &self,
42+ _ctx: &MigrationContext,
43+ db: &'a mut dyn Conn,
44+ ) -> anyhow::Result<Option<Transaction<'a>>> {
45+ if let Some(complete_query) = &self.complete {
46+ db.run(complete_query)?;
47+ }
48+
49+ Ok(None)
50+ }
51+
52+ fn update_schema(&self, _ctx: &MigrationContext, _schema: &mut Schema) {}
53+
54+ fn abort(&self, _ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
55+ if let Some(abort_query) = &self.abort {
56+ db.run(abort_query)?;
57+ }
58+
59+ Ok(())
60+ }
61+}
src/migrations/mod.rs+3
@@ -39,6 +39,9 @@ pub use create_enum::CreateEnum;
39mod remove_enum;
39mod remove_enum;
40pub use remove_enum::RemoveEnum;
40pub use remove_enum::RemoveEnum;
41
41
42+mod custom;
43+pub use custom::Custom;
44+
42mod add_foreign_key;
45mod add_foreign_key;
43pub use add_foreign_key::AddForeignKey;
46pub use add_foreign_key::AddForeignKey;
44
47
tests/common.rs+12
@@ -12,6 +12,7 @@ pub struct Test<'a> {
12 second_migration: Option<Migration>,
12 second_migration: Option<Migration>,
13 expect_failure: bool,
13 expect_failure: bool,
14
14
15+ clear_fn: Option<fn(&mut Client) -> ()>,
15 after_first_fn: Option<fn(&mut Client) -> ()>,
16 after_first_fn: Option<fn(&mut Client) -> ()>,
16 intermediate_fn: Option<fn(&mut Client, &mut Client) -> ()>,
17 intermediate_fn: Option<fn(&mut Client, &mut Client) -> ()>,
17 after_completion_fn: Option<fn(&mut Client) -> ()>,
18 after_completion_fn: Option<fn(&mut Client) -> ()>,
@@ -36,6 +37,7 @@ impl Test<'_> {
36 first_migration: None,
37 first_migration: None,
37 second_migration: None,
38 second_migration: None,
38 expect_failure: false,
39 expect_failure: false,
40+ clear_fn: None,
39 after_first_fn: None,
41 after_first_fn: None,
40 intermediate_fn: None,
42 intermediate_fn: None,
41 after_completion_fn: None,
43 after_completion_fn: None,
@@ -54,6 +56,12 @@ impl Test<'_> {
54 self
56 self
55 }
57 }
56
58
59+ #[allow(dead_code)]
60+ pub fn clear(&mut self, f: fn(&mut Client) -> ()) -> &mut Self {
61+ self.clear_fn = Some(f);
62+ self
63+ }
64+
57 #[allow(dead_code)]
65 #[allow(dead_code)]
58 pub fn after_first(&mut self, f: fn(&mut Client) -> ()) -> &mut Self {
66 pub fn after_first(&mut self, f: fn(&mut Client) -> ()) -> &mut Self {
59 self.after_first_fn = Some(f);
67 self.after_first_fn = Some(f);
@@ -115,6 +123,10 @@ impl Test<'_> {
115 print_subheading("Clearing database");
123 print_subheading("Clearing database");
116 self.reshape.remove().unwrap();
124 self.reshape.remove().unwrap();
117
125
126+ if let Some(clear_fn) = self.clear_fn {
127+ clear_fn(&mut self.old_db);
128+ }
129+
118 // Apply first migration, will automatically complete
130 // Apply first migration, will automatically complete
119 print_subheading("Applying first migration");
131 print_subheading("Applying first migration");
120 let first_migration = self
132 let first_migration = self
tests/custom.rsnew file+128
@@ -0,0 +1,128 @@
1+mod common;
2+use common::Test;
3+
4+#[test]
5+fn custom_enable_extension() {
6+ let mut test = Test::new("Custom migration");
7+
8+ test.clear(|db| {
9+ db.simple_query(
10+ "
11+ DROP EXTENSION IF EXISTS bloom;
12+ DROP EXTENSION IF EXISTS btree_gin;
13+ DROP EXTENSION IF EXISTS btree_gist;
14+ ",
15+ )
16+ .unwrap();
17+ });
18+
19+ test.first_migration(
20+ r#"
21+ name = "empty_migration"
22+
23+ [[actions]]
24+ type = "custom"
25+ "#,
26+ );
27+
28+ test.second_migration(
29+ r#"
30+ name = "enable_extensions"
31+
32+ [[actions]]
33+ type = "custom"
34+
35+ start = """
36+ CREATE EXTENSION IF NOT EXISTS bloom;
37+ CREATE EXTENSION IF NOT EXISTS btree_gin;
38+ """
39+
40+ complete = "CREATE EXTENSION IF NOT EXISTS btree_gist"
41+
42+ abort = """
43+ DROP EXTENSION IF EXISTS bloom;
44+ DROP EXTENSION IF EXISTS btree_gin;
45+ """
46+ "#,
47+ );
48+
49+ test.intermediate(|db, _| {
50+ let bloom_activated = !db
51+ .query("SELECT * FROM pg_extension WHERE extname = 'bloom'", &[])
52+ .unwrap()
53+ .is_empty();
54+ assert!(bloom_activated);
55+
56+ let btree_gin_activated = !db
57+ .query(
58+ "SELECT * FROM pg_extension WHERE extname = 'btree_gin'",
59+ &[],
60+ )
61+ .unwrap()
62+ .is_empty();
63+ assert!(btree_gin_activated);
64+
65+ let btree_gist_activated = !db
66+ .query(
67+ "SELECT * FROM pg_extension WHERE extname = 'btree_gist'",
68+ &[],
69+ )
70+ .unwrap()
71+ .is_empty();
72+ assert!(!btree_gist_activated);
73+ });
74+
75+ test.after_completion(|db| {
76+ let bloom_activated = !db
77+ .query("SELECT * FROM pg_extension WHERE extname = 'bloom'", &[])
78+ .unwrap()
79+ .is_empty();
80+ assert!(bloom_activated);
81+
82+ let btree_gin_activated = !db
83+ .query(
84+ "SELECT * FROM pg_extension WHERE extname = 'btree_gin'",
85+ &[],
86+ )
87+ .unwrap()
88+ .is_empty();
89+ assert!(btree_gin_activated);
90+
91+ let btree_gist_activated = !db
92+ .query(
93+ "SELECT * FROM pg_extension WHERE extname = 'btree_gist'",
94+ &[],
95+ )
96+ .unwrap()
97+ .is_empty();
98+ assert!(btree_gist_activated);
99+ });
100+
101+ test.after_abort(|db| {
102+ let bloom_activated = !db
103+ .query("SELECT * FROM pg_extension WHERE extname = 'bloom'", &[])
104+ .unwrap()
105+ .is_empty();
106+ assert!(!bloom_activated);
107+
108+ let btree_gin_activated = !db
109+ .query(
110+ "SELECT * FROM pg_extension WHERE extname = 'btree_gin'",
111+ &[],
112+ )
113+ .unwrap()
114+ .is_empty();
115+ assert!(!btree_gin_activated);
116+
117+ let btree_gist_activated = !db
118+ .query(
119+ "SELECT * FROM pg_extension WHERE extname = 'btree_gist'",
120+ &[],
121+ )
122+ .unwrap()
123+ .is_empty();
124+ assert!(!btree_gist_activated);
125+ });
126+
127+ test.run();
128+}