mirror of
https://github.com/ilyakooo0/reshape.git
synced 2024-11-22 01:09:15 +03:00
Add remove_foreign_key action
This commit is contained in:
parent
5984c1c507
commit
4e93a39be0
14
README.md
14
README.md
@ -22,6 +22,7 @@ Designed for Postgres 12 and later.
|
||||
- [Rename table](#rename-table)
|
||||
- [Remove table](#remove-table)
|
||||
- [Add foreign key](#add-foreign-key)
|
||||
- [Remove foreign key](#remove-foreign-key)
|
||||
- [Columns](#columns)
|
||||
- [Add column](#add-column)
|
||||
- [Alter column](#alter-column)
|
||||
@ -261,6 +262,19 @@ table = "items"
|
||||
referenced_columns = ["id"]
|
||||
```
|
||||
|
||||
#### Remove foreign key
|
||||
|
||||
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.
|
||||
|
||||
*Example: remove foreign key `items_user_id_fkey` from `users` table*
|
||||
|
||||
```toml
|
||||
[[actions]]
|
||||
type = "remove_foreign_key"
|
||||
table = "items"
|
||||
foreign_key = "items_user_id_fkey"
|
||||
```
|
||||
|
||||
### Columns
|
||||
|
||||
#### Add column
|
||||
|
@ -42,6 +42,9 @@ pub use remove_enum::RemoveEnum;
|
||||
mod add_foreign_key;
|
||||
pub use add_foreign_key::AddForeignKey;
|
||||
|
||||
mod remove_foreign_key;
|
||||
pub use remove_foreign_key::RemoveForeignKey;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Migration {
|
||||
pub name: String,
|
||||
|
91
src/migrations/remove_foreign_key.rs
Normal file
91
src/migrations/remove_foreign_key.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use super::{Action, MigrationContext};
|
||||
use crate::{
|
||||
db::{Conn, Transaction},
|
||||
schema::Schema,
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct RemoveForeignKey {
|
||||
table: String,
|
||||
foreign_key: String,
|
||||
}
|
||||
|
||||
#[typetag::serde(name = "remove_foreign_key")]
|
||||
impl Action for RemoveForeignKey {
|
||||
fn describe(&self) -> String {
|
||||
format!(
|
||||
"Removing foreign key \"{}\" from table \"{}\"",
|
||||
self.foreign_key, self.table
|
||||
)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_ctx: &MigrationContext,
|
||||
db: &mut dyn Conn,
|
||||
schema: &Schema,
|
||||
) -> anyhow::Result<()> {
|
||||
// The foreign key is only removed once the migration is completed.
|
||||
// Removing it earlier would be hard/undesirable for several reasons:
|
||||
// - Postgres doesn't have an easy way to temporarily disable a foreign key check.
|
||||
// If it did, we could disable the FK for the new schema.
|
||||
// - Even if we could, it probably wouldn't be a good idea as it would cause temporary
|
||||
// inconsistencies for the old schema which still expects the FK to hold.
|
||||
// - For the same reason, we can't remove the FK when the migration is first applied.
|
||||
// If the migration was to be aborted, then the FK would have to be recreated with
|
||||
// the risk that it would no longer be valid.
|
||||
|
||||
// Ensure foreign key exists
|
||||
let table = schema.get_table(db, &self.table)?;
|
||||
let fk_exists = !db
|
||||
.query(&format!(
|
||||
r#"
|
||||
SELECT constraint_name
|
||||
FROM information_schema.table_constraints
|
||||
WHERE
|
||||
constraint_type = 'FOREIGN KEY' AND
|
||||
table_name = '{table_name}' AND
|
||||
constraint_name = '{foreign_key}'
|
||||
"#,
|
||||
table_name = table.real_name,
|
||||
foreign_key = self.foreign_key,
|
||||
))
|
||||
.context("failed to check for foreign key")?
|
||||
.is_empty();
|
||||
|
||||
if !fk_exists {
|
||||
return Err(anyhow!(
|
||||
"no foreign key \"{}\" exists on table \"{}\"",
|
||||
self.foreign_key,
|
||||
self.table
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete<'a>(
|
||||
&self,
|
||||
_ctx: &MigrationContext,
|
||||
db: &'a mut dyn Conn,
|
||||
) -> anyhow::Result<Option<Transaction<'a>>> {
|
||||
db.run(&format!(
|
||||
r#"
|
||||
ALTER TABLE {table}
|
||||
DROP CONSTRAINT IF EXISTS {foreign_key}
|
||||
"#,
|
||||
table = self.table,
|
||||
foreign_key = self.foreign_key,
|
||||
))
|
||||
.context("failed to remove foreign key")?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn update_schema(&self, _ctx: &MigrationContext, _schema: &mut Schema) {}
|
||||
|
||||
fn abort(&self, _ctx: &MigrationContext, _db: &mut dyn Conn) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
124
tests/remove_foreign_key.rs
Normal file
124
tests/remove_foreign_key.rs
Normal file
@ -0,0 +1,124 @@
|
||||
mod common;
|
||||
use common::Test;
|
||||
|
||||
#[test]
|
||||
fn remove_foreign_key() {
|
||||
let mut test = Test::new("Remove foreign key");
|
||||
|
||||
test.first_migration(
|
||||
r#"
|
||||
name = "create_tables"
|
||||
|
||||
[[actions]]
|
||||
type = "create_table"
|
||||
name = "users"
|
||||
primary_key = ["id"]
|
||||
|
||||
[[actions.columns]]
|
||||
name = "id"
|
||||
type = "INTEGER"
|
||||
|
||||
[[actions]]
|
||||
type = "create_table"
|
||||
name = "items"
|
||||
primary_key = ["id"]
|
||||
|
||||
[[actions.columns]]
|
||||
name = "id"
|
||||
type = "INTEGER"
|
||||
|
||||
[[actions.columns]]
|
||||
name = "user_id"
|
||||
type = "INTEGER"
|
||||
|
||||
[[actions.foreign_keys]]
|
||||
columns = ["user_id"]
|
||||
referenced_table = "users"
|
||||
referenced_columns = ["id"]
|
||||
"#,
|
||||
);
|
||||
|
||||
test.second_migration(
|
||||
r#"
|
||||
name = "remove_foreign_key"
|
||||
|
||||
[[actions]]
|
||||
type = "remove_foreign_key"
|
||||
table = "items"
|
||||
foreign_key = "items_user_id_fkey"
|
||||
"#,
|
||||
);
|
||||
|
||||
test.after_first(|db| {
|
||||
// Insert some test users
|
||||
db.simple_query("INSERT INTO users (id) VALUES (1), (2)")
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
test.intermediate(|old_db, new_db| {
|
||||
// Ensure items can't be inserted if they don't reference valid users
|
||||
// The foreign key is only removed when the migration is completed so
|
||||
// it should still be enforced for the new and old schema.
|
||||
let result = old_db.simple_query("INSERT INTO items (id, user_id) VALUES (3, 3)");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected insert against old schema to fail"
|
||||
);
|
||||
|
||||
let result = new_db.simple_query("INSERT INTO items (id, user_id) VALUES (3, 3)");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"expected insert against new schema to fail"
|
||||
);
|
||||
});
|
||||
|
||||
test.after_completion(|db| {
|
||||
// Ensure items can be inserted even if they don't reference valid users
|
||||
let result = db
|
||||
.simple_query("INSERT INTO items (id, user_id) VALUES (5, 3)")
|
||||
.map(|_| ());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"expected insert to not fail, got {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
// Ensure foreign key doesn't exist
|
||||
let foreign_keys = db
|
||||
.query(
|
||||
"
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name='items';
|
||||
",
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
foreign_keys.is_empty(),
|
||||
"expected no foreign keys to exist on items table"
|
||||
);
|
||||
});
|
||||
|
||||
test.after_abort(|db| {
|
||||
// Ensure items can't be inserted if they don't reference valid users
|
||||
let result = db.simple_query("INSERT INTO items (id, user_id) VALUES (3, 3)");
|
||||
assert!(result.is_err(), "expected insert to fail");
|
||||
|
||||
// Ensure foreign key still exists
|
||||
let fk_exists = !db
|
||||
.query(
|
||||
"
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name='items';
|
||||
",
|
||||
&[],
|
||||
)
|
||||
.unwrap()
|
||||
.is_empty();
|
||||
assert!(fk_exists);
|
||||
});
|
||||
|
||||
test.run()
|
||||
}
|
Loading…
Reference in New Issue
Block a user