@@ -3,7 +3,7 @@ use crate::{
3 db::{Conn, Transaction},
3 db::{Conn, Transaction},
4 schema::Schema,
4 schema::Schema,
6−use anyhow::{anyhow, bail, Context};
6+use anyhow::{bail, Context};
7use serde::{Deserialize, Serialize};
7use serde::{Deserialize, Serialize};
9#[derive(Serialize, Deserialize, Debug)]
9#[derive(Serialize, Deserialize, Debug)]
@@ -33,33 +33,6 @@ impl RemoveColumn {
33 self.column
33 self.column
37− fn reverse_trigger_name(&self, ctx: &MigrationContext) -> String {
39− "{}_remove_column_{}_{}_rev",
46− fn not_null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
48− "{}_remove_column_{}_{}_nn",
55− fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
57− "{}_add_column_not_null_{}_{}",
65#[typetag::serde(name = "remove_column")]
38#[typetag::serde(name = "remove_column")]
@@ -78,9 +51,6 @@ impl Action for RemoveColumn {
78 schema: &Schema,
51 schema: &Schema,
79 ) -> anyhow::Result<()> {
52 ) -> anyhow::Result<()> {
80 let table = schema.get_table(db, &self.table)?;
53 let table = schema.get_table(db, &self.table)?;
82− .get_column(&self.column)
83− .ok_or_else(|| anyhow!("no such column {} exists", self.column))?;
85 // Add down trigger
55 // Add down trigger
86 if let Some(down) = &self.down {
56 if let Some(down) = &self.down {
@@ -139,75 +109,18 @@ impl Action for RemoveColumn {
140 let from_table = schema.get_table(db, &from_table)?;
110 let from_table = schema.get_table(db, &from_table)?;
142− let maybe_null_check = if !column.nullable {
112+ let declarations: Vec<String> = from_table
143− // Replace NOT NULL constraint with a constraint trigger that only triggers on the old schema.
144− // We will add a null check to the down function on the new schema below as well to cover both cases.
145− // As we are using a complex down function, we must remove the NOT NULL check for the new schema.
146− // NOT NULL is not checked at the end of a transaction, but immediately upon update.
147− let query = format!(
149− CREATE OR REPLACE FUNCTION {trigger_name}()
150− RETURNS TRIGGER AS $$
152− IF NOT reshape.is_new_schema() THEN
153− IF NEW.{column} IS NULL THEN
154− RAISE EXCEPTION '{column} can not be null';
159− $$ language 'plpgsql';
161− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{table}";
163− CREATE CONSTRAINT TRIGGER "{trigger_name}"
164− AFTER INSERT OR UPDATE
167− EXECUTE PROCEDURE {trigger_name}();
170− trigger_name = self.not_null_constraint_trigger_name(ctx),
171− column = self.column,
174− .context("failed to create null constraint trigger")?;
179− ALTER COLUMN {column}
183− column = self.column
185− .context("failed to remove column not null constraint")?;
189− IF {value} IS NULL THEN
190− RAISE EXCEPTION '{column_name} can not be null';
193− column_name = self.column,
199− let into_variables = from_table
202 .map(|column| {
115 .map(|column| {
204− "NEW.{real_name} AS {alias}",
117+ "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
118+ table = from_table.real_name,
205 alias = column.name,
119 alias = column.name,
206 real_name = column.real_name,
120 real_name = column.real_name,
209− .collect::<Vec<String>>()
123+ .collect();
212 let query = format!(
125 let query = format!(
@@ -217,21 +130,11 @@ impl Action for RemoveColumn {
218 IF reshape.is_new_schema() THEN
131 IF reshape.is_new_schema() THEN
220− {from_table} record;
133+ {declarations}
222− SELECT {into_variables}
227− -- Don't trigger reverse trigger when making this update
228− perform set_config('reshape.disable_triggers', 'TRUE', TRUE);
230 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
135 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
231 SET "{column_name}" = {value}
136 SET "{column_name}" = {value}
232 WHERE {where};
137 WHERE {where};
234− perform set_config('reshape.disable_triggers', '', TRUE);
237 RETURN NEW;
140 RETURN NEW;
@@ -242,80 +145,12 @@ impl Action for RemoveColumn {
242 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
145 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
244 changed_table = self.table,
147 changed_table = self.table,
245− from_table = from_table.name,
246 from_table_real = from_table.real_name,
148 from_table_real = from_table.real_name,
247 column_name = self.column,
149 column_name = self.column,
248 trigger_name = self.trigger_name(ctx),
150 trigger_name = self.trigger_name(ctx),
151+ declarations = declarations.join("\n"),
250 db.run(&query).context("failed to create down trigger")?;
153 db.run(&query).context("failed to create down trigger")?;
252− let changed_into_variables = table
257− "NEW.{real_name} AS {alias}",
258− alias = column.name,
259− real_name = column.real_name,
262− .collect::<Vec<String>>()
265− let from_table_columns = from_table
268− .map(|column| format!("{} as {}", column.real_name, column.name))
269− .collect::<Vec<String>>()
272− let query = format!(
274− CREATE OR REPLACE FUNCTION {trigger_name}()
275− RETURNS TRIGGER AS $$
276− #variable_conflict use_variable
278− IF reshape.is_new_schema() AND NOT current_setting('reshape.disable_triggers', TRUE) = 'TRUE' THEN
280− {changed_table} record;
283− SELECT {changed_into_variables}
284− INTO {changed_table};
289− SELECT {from_table_columns}
290− FROM public.{from_table_real}
295− {from_table} record;
297− {from_table} := __temp_row;
298− NEW.{column_name_real} = {value};
304− $$ language 'plpgsql';
306− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{changed_table_real}";
307− CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{changed_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
309− changed_table = table.name,
310− changed_table_real = table.real_name,
311− from_table = from_table.name,
312− from_table_real = from_table.real_name,
313− column_name_real = column.real_name,
314− trigger_name = self.reverse_trigger_name(ctx),
315− // declarations = declarations.join("\n"),
318− .context("failed to create reverse down trigger")?;
@@ -341,20 +176,25 @@ impl Action for RemoveColumn {
343 // Remove column, function and trigger
178 // Remove column, function and trigger
179+ let trigger_table = match &self.down {
180+ Some(Transformation::Update {
344 let query = format!(
187 let query = format!(
346 ALTER TABLE "{table}"
189 ALTER TABLE "{table}"
347 DROP COLUMN IF EXISTS "{column}";
190 DROP COLUMN IF EXISTS "{column}";
349− DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
192+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
350− DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
351− DROP FUNCTION IF EXISTS "{null_trigger_name}" CASCADE;
193+ DROP FUNCTION IF EXISTS "{trigger_name}";
353 table = self.table,
195 table = self.table,
354 column = self.column,
196 column = self.column,
355 trigger_name = self.trigger_name(ctx),
197 trigger_name = self.trigger_name(ctx),
356− reverse_trigger_name = self.reverse_trigger_name(ctx),
357− null_trigger_name = self.not_null_constraint_trigger_name(ctx),
359 db.run(&query)
199 db.run(&query)
360 .context("failed to drop column and down trigger")?;
200 .context("failed to drop column and down trigger")?;
@@ -371,81 +211,21 @@ impl Action for RemoveColumn {
373 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
213 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
374− // We might have temporaily removed the NOT NULL check and have to reinstate it
375− let has_not_null_function = !db
379− FROM information_schema.routines
380− WHERE routine_schema = 'public'
381− AND routine_name = $1
383− &[&self.not_null_constraint_trigger_name(ctx)],
385− .context("failed to get any NOT NULL function")?
388− if has_not_null_function {
389− // Make column NOT NULL again without taking any long lived locks with a temporary constraint
390− let query = format!(
392− ALTER TABLE "{table}"
393− ADD CONSTRAINT "{constraint_name}"
394− CHECK ("{column}" IS NOT NULL) NOT VALID
397− constraint_name = self.not_null_constraint_name(ctx),
398− column = self.column,
401− .context("failed to add NOT NULL constraint")?;
403− let query = format!(
405− ALTER TABLE "{table}"
406− VALIDATE CONSTRAINT "{constraint_name}"
409− constraint_name = self.not_null_constraint_name(ctx),
412− .context("failed to validate NOT NULL constraint")?;
414− // This ALTER TABLE call will not require any exclusive locks as it can use the validated constraint from above
418− ALTER COLUMN {column}
422− column = self.column
424− .context("failed to reinstate column NOT NULL")?;
426− // Drop the temporary constraint
427− let query = format!(
429− ALTER TABLE "{table}"
430− DROP CONSTRAINT "{constraint_name}"
433− constraint_name = self.not_null_constraint_name(ctx),
436− .context("failed to drop NOT NULL constraint")?;
439 // Remove function and trigger
214 // Remove function and trigger
215+ let trigger_table = match &self.down {
216+ Some(Transformation::Update {
440 db.run(&format!(
223 db.run(&format!(
442− DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
225+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
443− DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
444− DROP FUNCTION IF EXISTS "{null_trigger_name}" CASCADE;
226+ DROP FUNCTION IF EXISTS "{trigger_name}";
446 trigger_name = self.trigger_name(ctx),
228 trigger_name = self.trigger_name(ctx),
447− reverse_trigger_name = self.reverse_trigger_name(ctx),
448− null_trigger_name = self.not_null_constraint_trigger_name(ctx),
450 .context("failed to drop down trigger")?;
230 .context("failed to drop down trigger")?;