@@ -34,7 +34,16 @@ impl RemoveColumn {
37+ fn reverse_trigger_name(&self, ctx: &MigrationContext) -> String {
39+ "{}_remove_column_{}_{}_rev",
37− fn null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
46+ fn not_null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
39 "{}_remove_column_{}_{}_nn",
48 "{}_remove_column_{}_{}_nn",
40 ctx.prefix(),
49 ctx.prefix(),
@@ -42,6 +51,15 @@ impl RemoveColumn {
42 self.column
51 self.column
55+ fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
57+ "{}_add_column_not_null_{}_{}",
47#[typetag::serde(name = "remove_column")]
65#[typetag::serde(name = "remove_column")]
@@ -121,21 +139,9 @@ impl Action for RemoveColumn {
122 let from_table = schema.get_table(db, &from_table)?;
140 let from_table = schema.get_table(db, &from_table)?;
124− let declarations: Vec<String> = from_table
129− "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
130− table = from_table.real_name,
131− alias = column.name,
132− real_name = column.real_name,
137 let maybe_null_check = if !column.nullable {
142 let maybe_null_check = if !column.nullable {
138 // Replace NOT NULL constraint with a constraint trigger that only triggers on the old schema.
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.
139 // As we are using a complex down function, we must remove the NOT NULL check for the new schema.
145 // As we are using a complex down function, we must remove the NOT NULL check for the new schema.
140 // NOT NULL is not checked at the end of a transaction, but immediately upon update.
146 // NOT NULL is not checked at the end of a transaction, but immediately upon update.
141 let query = format!(
147 let query = format!(
@@ -161,7 +167,7 @@ impl Action for RemoveColumn {
161 EXECUTE PROCEDURE {trigger_name}();
167 EXECUTE PROCEDURE {trigger_name}();
163 table = self.table,
169 table = self.table,
164− trigger_name = self.null_constraint_trigger_name(ctx),
170+ trigger_name = self.not_null_constraint_trigger_name(ctx),
165 column = self.column,
171 column = self.column,
167 db.run(&query)
173 db.run(&query)
@@ -190,6 +196,19 @@ impl Action for RemoveColumn {
190 "".to_string()
196 "".to_string()
199+ let into_variables = from_table
204+ "NEW.{real_name} AS {alias}",
205+ alias = column.name,
206+ real_name = column.real_name,
209+ .collect::<Vec<String>>()
193 let query = format!(
212 let query = format!(
195 CREATE OR REPLACE FUNCTION {trigger_name}()
214 CREATE OR REPLACE FUNCTION {trigger_name}()
@@ -198,12 +217,21 @@ impl Action for RemoveColumn {
199 IF reshape.is_new_schema() THEN
218 IF reshape.is_new_schema() THEN
201− {declarations}
220+ {from_table} record;
222+ SELECT {into_variables}
203 {maybe_null_check}
225 {maybe_null_check}
227+ -- Don't trigger reverse trigger when making this update
228+ perform set_config('reshape.disable_triggers', 'TRUE', TRUE);
204 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
230 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
205 SET "{column_name}" = {value}
231 SET "{column_name}" = {value}
206 WHERE {where};
232 WHERE {where};
234+ perform set_config('reshape.disable_triggers', '', TRUE);
209 RETURN NEW;
237 RETURN NEW;
@@ -214,17 +242,83 @@ impl Action for RemoveColumn {
214 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
242 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
216 changed_table = self.table,
244 changed_table = self.table,
245+ from_table = from_table.name,
217 from_table_real = from_table.real_name,
246 from_table_real = from_table.real_name,
218 column_name = self.column,
247 column_name = self.column,
219 trigger_name = self.trigger_name(ctx),
248 trigger_name = self.trigger_name(ctx),
220− declarations = declarations.join("\n"),
222 db.run(&query).context("failed to create down trigger")?;
250 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")?;
226− if !column.nullable {}
@@ -247,29 +341,20 @@ impl Action for RemoveColumn {
249 // Remove column, function and trigger
343 // Remove column, function and trigger
250− let trigger_table = match &self.down {
251− Some(Transformation::Update {
258 let query = format!(
344 let query = format!(
260 ALTER TABLE "{table}"
346 ALTER TABLE "{table}"
261 DROP COLUMN IF EXISTS "{column}";
347 DROP COLUMN IF EXISTS "{column}";
263− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
349+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
264− DROP FUNCTION IF EXISTS "{trigger_name}";
350+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
266− DROP TRIGGER IF EXISTS "{null_trigger_name}" ON "{table}";
267− DROP FUNCTION IF EXISTS "{null_trigger_name}";
351+ DROP FUNCTION IF EXISTS "{null_trigger_name}" CASCADE;
269 table = self.table,
353 table = self.table,
270 column = self.column,
354 column = self.column,
271 trigger_name = self.trigger_name(ctx),
355 trigger_name = self.trigger_name(ctx),
356+ reverse_trigger_name = self.reverse_trigger_name(ctx),
272− null_trigger_name = self.null_constraint_trigger_name(ctx),
357+ null_trigger_name = self.not_null_constraint_trigger_name(ctx),
274 db.run(&query)
359 db.run(&query)
275 .context("failed to drop column and down trigger")?;
360 .context("failed to drop column and down trigger")?;
@@ -286,38 +371,81 @@ impl Action for RemoveColumn {
288 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
373 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
289− // We might have removed the NOT NULL check so we reinstate it
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
290− db.run(&format!(
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!(
292− ALTER TABLE {table}
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
293− ALTER COLUMN {column}
418+ ALTER COLUMN {column}
294− SET NOT NULL
419+ SET NOT NULL
296− table = self.table,
421+ table = self.table,
297− column = self.column
422+ column = self.column
299− .context("failed to reinstate column not null constraint")?;
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")?;
301 // Remove function and trigger
439 // Remove function and trigger
302− let trigger_table = match &self.down {
303− Some(Transformation::Update {
310 db.run(&format!(
440 db.run(&format!(
312− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
442+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
313− DROP FUNCTION IF EXISTS "{trigger_name}";
443+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
315− DROP TRIGGER IF EXISTS "{null_trigger_name}" ON "{table}";
316− DROP FUNCTION IF EXISTS "{null_trigger_name}";
444+ DROP FUNCTION IF EXISTS "{null_trigger_name}" CASCADE;
319 trigger_name = self.trigger_name(ctx),
446 trigger_name = self.trigger_name(ctx),
447+ reverse_trigger_name = self.reverse_trigger_name(ctx),
320− null_trigger_name = self.null_constraint_trigger_name(ctx),
448+ null_trigger_name = self.not_null_constraint_trigger_name(ctx),
322 .context("failed to drop down trigger")?;
450 .context("failed to drop down trigger")?;