on main help sign in
Use safe NOT NULL checking during abort for complex remove column action
4c316aefabianlindfors
src/migrations/remove_column.rs+78−18
@@ -43,7 +43,7 @@ impl RemoveColumn {
43 )
43 )
44 }
44 }
45
45
46− fn null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
46+ fn not_null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
47 format!(
47 format!(
48 "{}_remove_column_{}_{}_nn",
48 "{}_remove_column_{}_{}_nn",
49 ctx.prefix(),
49 ctx.prefix(),
@@ -51,6 +51,15 @@ impl RemoveColumn {
51 self.column
51 self.column
52 )
52 )
53 }
53 }
54+
55+ fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
56+ format!(
57+ "{}_add_column_not_null_{}_{}",
58+ ctx.prefix(),
59+ self.table,
60+ self.column
61+ )
62+ }
54}
63}
55
64
56#[typetag::serde(name = "remove_column")]
65#[typetag::serde(name = "remove_column")]
@@ -132,6 +141,7 @@ impl Action for RemoveColumn {
132
141
133 let maybe_null_check = if !column.nullable {
142 let maybe_null_check = if !column.nullable {
134 // 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.
135 // 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.
136 // 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.
137 let query = format!(
147 let query = format!(
@@ -157,7 +167,7 @@ impl Action for RemoveColumn {
157 EXECUTE PROCEDURE {trigger_name}();
167 EXECUTE PROCEDURE {trigger_name}();
158 "#,
168 "#,
159 table = self.table,
169 table = self.table,
160− trigger_name = self.null_constraint_trigger_name(ctx),
170+ trigger_name = self.not_null_constraint_trigger_name(ctx),
161 column = self.column,
171 column = self.column,
162 );
172 );
163 db.run(&query)
173 db.run(&query)
@@ -236,7 +246,6 @@ impl Action for RemoveColumn {
236 from_table_real = from_table.real_name,
246 from_table_real = from_table.real_name,
237 column_name = self.column,
247 column_name = self.column,
238 trigger_name = self.trigger_name(ctx),
248 trigger_name = self.trigger_name(ctx),
239− // declarations = declarations.join("\n"),
240 );
249 );
241 db.run(&query).context("failed to create down trigger")?;
250 db.run(&query).context("failed to create down trigger")?;
242
251
@@ -310,8 +319,6 @@ impl Action for RemoveColumn {
310 }
319 }
311 }
320 }
312
321
313− if !column.nullable {}
314−
315 Ok(())
322 Ok(())
316 }
323 }
317
324
@@ -347,7 +354,7 @@ impl Action for RemoveColumn {
347 column = self.column,
354 column = self.column,
348 trigger_name = self.trigger_name(ctx),
355 trigger_name = self.trigger_name(ctx),
349 reverse_trigger_name = self.reverse_trigger_name(ctx),
356 reverse_trigger_name = self.reverse_trigger_name(ctx),
350− null_trigger_name = self.null_constraint_trigger_name(ctx),
357+ null_trigger_name = self.not_null_constraint_trigger_name(ctx),
351 );
358 );
352 db.run(&query)
359 db.run(&query)
353 .context("failed to drop column and down trigger")?;
360 .context("failed to drop column and down trigger")?;
@@ -364,17 +371,70 @@ impl Action for RemoveColumn {
364 }
371 }
365
372
366 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
373 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
367− // 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
376+ .query_with_params(
377+ "
378+ SELECT routine_name
379+ FROM information_schema.routines
380+ WHERE routine_schema = 'public'
381+ AND routine_name = $1
382+ ",
383+ &[&self.not_null_constraint_trigger_name(ctx)],
384+ )
385+ .context("failed to get any NOT NULL function")?
386+ .is_empty();
387+
388+ if has_not_null_function {
389+ // Make column NOT NULL again without taking any long lived locks with a temporary constraint
368− db.run(&format!(
390+ let query = format!(
369− r#"
391+ r#"
392+ ALTER TABLE "{table}"
393+ ADD CONSTRAINT "{constraint_name}"
394+ CHECK ("{column}" IS NOT NULL) NOT VALID
395+ "#,
396+ table = self.table,
397+ constraint_name = self.not_null_constraint_name(ctx),
398+ column = self.column,
399+ );
400+ db.run(&query)
401+ .context("failed to add NOT NULL constraint")?;
402+
403+ let query = format!(
404+ r#"
370− ALTER TABLE {table}
405+ ALTER TABLE "{table}"
406+ VALIDATE CONSTRAINT "{constraint_name}"
407+ "#,
408+ table = self.table,
409+ constraint_name = self.not_null_constraint_name(ctx),
410+ );
411+ db.run(&query)
412+ .context("failed to validate NOT NULL constraint")?;
413+
414+ // This ALTER TABLE call will not require any exclusive locks as it can use the validated constraint from above
415+ db.run(&format!(
416+ r#"
417+ ALTER TABLE {table}
371− ALTER COLUMN {column}
418+ ALTER COLUMN {column}
372− SET NOT NULL
419+ SET NOT NULL
373− "#,
420+ "#,
374− table = self.table,
421+ table = self.table,
375− column = self.column
422+ column = self.column
376− ))
423+ ))
377− .context("failed to reinstate column not null constraint")?;
424+ .context("failed to reinstate column NOT NULL")?;
425+
426+ // Drop the temporary constraint
427+ let query = format!(
428+ r#"
429+ ALTER TABLE "{table}"
430+ DROP CONSTRAINT "{constraint_name}"
431+ "#,
432+ table = self.table,
433+ constraint_name = self.not_null_constraint_name(ctx),
434+ );
435+ db.run(&query)
436+ .context("failed to drop NOT NULL constraint")?;
437+ }
378
438
379 // Remove function and trigger
439 // Remove function and trigger
380 db.run(&format!(
440 db.run(&format!(
@@ -385,7 +445,7 @@ impl Action for RemoveColumn {
385 "#,
445 "#,
386 trigger_name = self.trigger_name(ctx),
446 trigger_name = self.trigger_name(ctx),
387 reverse_trigger_name = self.reverse_trigger_name(ctx),
447 reverse_trigger_name = self.reverse_trigger_name(ctx),
388− null_trigger_name = self.null_constraint_trigger_name(ctx),
448+ null_trigger_name = self.not_null_constraint_trigger_name(ctx),
389 ))
449 ))
390 .context("failed to drop down trigger")?;
450 .context("failed to drop down trigger")?;
391
451