on main help sign in
Add remove_foreign_key action
commit4e93a39be0cd31b66a230ef2261b6e737826897d authorfabianlindfors <fabian@flapplabs.se> dateThu 27 Jan 2022, 23:33 (5 years ago) parent5984c1c
4 files changed+232
README.md+14 src/migrations/mod.rs+3 src/migrations/remove_foreign_key.rs+91 tests/remove_foreign_key.rs+124
README.md+14
@@ -22,6 +22,7 @@ Designed for Postgres 12 and later.
2222 - [Rename table](#rename-table)
2323 - [Remove table](#remove-table)
2424 - [Add foreign key](#add-foreign-key)
25+ - [Remove foreign key](#remove-foreign-key)
2526 - [Columns](#columns)
2627 - [Add column](#add-column)
2728 - [Alter column](#alter-column)
@@ -261,6 +262,19 @@ table = "items"
261262 referenced_columns = ["id"]
262263```
263264
265+#### Remove foreign key
266+
267+The `remove_foreign_key` action will remove an existing foreign key. The foreign key will only be removed once the migration is completed, which means that your new application must continue to adhere to the foreign key constraint.
268+
269+*Example: remove foreign key `items_user_id_fkey` from `users` table*
270+
271+```toml
272+[[actions]]
273+type = "remove_foreign_key"
274+table = "items"
275+foreign_key = "items_user_id_fkey"
276+```
277+
264278### Columns
265279
266280#### Add column
src/migrations/mod.rs+3
@@ -42,6 +42,9 @@ pub use remove_enum::RemoveEnum;
4242mod add_foreign_key;
4343pub use add_foreign_key::AddForeignKey;
4444
45+mod remove_foreign_key;
46+pub use remove_foreign_key::RemoveForeignKey;
47+
4548#[derive(Serialize, Deserialize, Debug)]
4649pub struct Migration {
4750 pub name: String,
src/migrations/remove_foreign_key.rsnew file+91
@@ -0,0 +1,91 @@
1+use super::{Action, MigrationContext};
2+use crate::{
3+ db::{Conn, Transaction},
4+ schema::Schema,
5+};
6+use anyhow::{anyhow, Context};
7+use serde::{Deserialize, Serialize};
8+
9+#[derive(Serialize, Deserialize, Debug)]
10+pub struct RemoveForeignKey {
11+ table: String,
12+ foreign_key: String,
13+}
14+
15+#[typetag::serde(name = "remove_foreign_key")]
16+impl Action for RemoveForeignKey {
17+ fn describe(&self) -> String {
18+ format!(
19+ "Removing foreign key \"{}\" from table \"{}\"",
20+ self.foreign_key, self.table
21+ )
22+ }
23+
24+ fn run(
25+ &self,
26+ _ctx: &MigrationContext,
27+ db: &mut dyn Conn,
28+ schema: &Schema,
29+ ) -> anyhow::Result<()> {
30+ // The foreign key is only removed once the migration is completed.
31+ // Removing it earlier would be hard/undesirable for several reasons:
32+ // - Postgres doesn't have an easy way to temporarily disable a foreign key check.
33+ // If it did, we could disable the FK for the new schema.
34+ // - Even if we could, it probably wouldn't be a good idea as it would cause temporary
35+ // inconsistencies for the old schema which still expects the FK to hold.
36+ // - For the same reason, we can't remove the FK when the migration is first applied.
37+ // If the migration was to be aborted, then the FK would have to be recreated with
38+ // the risk that it would no longer be valid.
39+
40+ // Ensure foreign key exists
41+ let table = schema.get_table(db, &self.table)?;
42+ let fk_exists = !db
43+ .query(&format!(
44+ r#"
45+ SELECT constraint_name
46+ FROM information_schema.table_constraints
47+ WHERE
48+ constraint_type = 'FOREIGN KEY' AND
49+ table_name = '{table_name}' AND
50+ constraint_name = '{foreign_key}'
51+ "#,
52+ table_name = table.real_name,
53+ foreign_key = self.foreign_key,
54+ ))
55+ .context("failed to check for foreign key")?
56+ .is_empty();
57+
58+ if !fk_exists {
59+ return Err(anyhow!(
60+ "no foreign key \"{}\" exists on table \"{}\"",
61+ self.foreign_key,
62+ self.table
63+ ));
64+ }
65+
66+ Ok(())
67+ }
68+
69+ fn complete<'a>(
70+ &self,
71+ _ctx: &MigrationContext,
72+ db: &'a mut dyn Conn,
73+ ) -> anyhow::Result<Option<Transaction<'a>>> {
74+ db.run(&format!(
75+ r#"
76+ ALTER TABLE {table}
77+ DROP CONSTRAINT IF EXISTS {foreign_key}
78+ "#,
79+ table = self.table,
80+ foreign_key = self.foreign_key,
81+ ))
82+ .context("failed to remove foreign key")?;
83+ Ok(None)
84+ }
85+
86+ fn update_schema(&self, _ctx: &MigrationContext, _schema: &mut Schema) {}
87+
88+ fn abort(&self, _ctx: &MigrationContext, _db: &mut dyn Conn) -> anyhow::Result<()> {
89+ Ok(())
90+ }
91+}
tests/remove_foreign_key.rsnew file+124
@@ -0,0 +1,124 @@
1+mod common;
2+use common::Test;
3+
4+#[test]
5+fn remove_foreign_key() {
6+ let mut test = Test::new("Remove foreign key");
7+
8+ test.first_migration(
9+ r#"
10+ name = "create_tables"
11+
12+ [[actions]]
13+ type = "create_table"
14+ name = "users"
15+ primary_key = ["id"]
16+
17+ [[actions.columns]]
18+ name = "id"
19+ type = "INTEGER"
20+
21+ [[actions]]
22+ type = "create_table"
23+ name = "items"
24+ primary_key = ["id"]
25+
26+ [[actions.columns]]
27+ name = "id"
28+ type = "INTEGER"
29+
30+ [[actions.columns]]
31+ name = "user_id"
32+ type = "INTEGER"
33+
34+ [[actions.foreign_keys]]
35+ columns = ["user_id"]
36+ referenced_table = "users"
37+ referenced_columns = ["id"]
38+ "#,
39+ );
40+
41+ test.second_migration(
42+ r#"
43+ name = "remove_foreign_key"
44+
45+ [[actions]]
46+ type = "remove_foreign_key"
47+ table = "items"
48+ foreign_key = "items_user_id_fkey"
49+ "#,
50+ );
51+
52+ test.after_first(|db| {
53+ // Insert some test users
54+ db.simple_query("INSERT INTO users (id) VALUES (1), (2)")
55+ .unwrap();
56+ });
57+
58+ test.intermediate(|old_db, new_db| {
59+ // Ensure items can't be inserted if they don't reference valid users
60+ // The foreign key is only removed when the migration is completed so
61+ // it should still be enforced for the new and old schema.
62+ let result = old_db.simple_query("INSERT INTO items (id, user_id) VALUES (3, 3)");
63+ assert!(
64+ result.is_err(),
65+ "expected insert against old schema to fail"
66+ );
67+
68+ let result = new_db.simple_query("INSERT INTO items (id, user_id) VALUES (3, 3)");
69+ assert!(
70+ result.is_err(),
71+ "expected insert against new schema to fail"
72+ );
73+ });
74+
75+ test.after_completion(|db| {
76+ // Ensure items can be inserted even if they don't reference valid users
77+ let result = db
78+ .simple_query("INSERT INTO items (id, user_id) VALUES (5, 3)")
79+ .map(|_| ());
80+ assert!(
81+ result.is_ok(),
82+ "expected insert to not fail, got {:?}",
83+ result
84+ );
85+
86+ // Ensure foreign key doesn't exist
87+ let foreign_keys = db
88+ .query(
89+ "
90+ SELECT tc.constraint_name
91+ FROM information_schema.table_constraints AS tc
92+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name='items';
93+ ",
94+ &[],
95+ )
96+ .unwrap();
97+ assert!(
98+ foreign_keys.is_empty(),
99+ "expected no foreign keys to exist on items table"
100+ );
101+ });
102+
103+ test.after_abort(|db| {
104+ // Ensure items can't be inserted if they don't reference valid users
105+ let result = db.simple_query("INSERT INTO items (id, user_id) VALUES (3, 3)");
106+ assert!(result.is_err(), "expected insert to fail");
107+
108+ // Ensure foreign key still exists
109+ let fk_exists = !db
110+ .query(
111+ "
112+ SELECT tc.constraint_name
113+ FROM information_schema.table_constraints AS tc
114+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name='items';
115+ ",
116+ &[],
117+ )
118+ .unwrap()
119+ .is_empty();
120+ assert!(fk_exists);
121+ });
122+
123+ test.run()
124+}