@@ -3,7 +3,7 @@ use crate::{
3 db::{Conn, Transaction},
3 db::{Conn, Transaction},
4 schema::Schema,
4 schema::Schema,
6−use anyhow::{bail, Context};
6+use anyhow::{anyhow, bail, Context};
7use serde::{Deserialize, Serialize};
7use serde::{Deserialize, Serialize};
9#[derive(Serialize, Deserialize, Debug)]
9#[derive(Serialize, Deserialize, Debug)]
@@ -33,6 +33,15 @@ impl RemoveColumn {
33 self.column
33 self.column
37+ fn null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
39+ "{}_remove_column_{}_{}_nn",
38#[typetag::serde(name = "remove_column")]
47#[typetag::serde(name = "remove_column")]
@@ -51,6 +60,9 @@ impl Action for RemoveColumn {
51 schema: &Schema,
60 schema: &Schema,
52 ) -> anyhow::Result<()> {
61 ) -> anyhow::Result<()> {
53 let table = schema.get_table(db, &self.table)?;
62 let table = schema.get_table(db, &self.table)?;
64+ .get_column(&self.column)
65+ .ok_or_else(|| anyhow!("no such column {} exists", self.column))?;
55 // Add down trigger
67 // Add down trigger
56 if let Some(down) = &self.down {
68 if let Some(down) = &self.down {
@@ -122,6 +134,62 @@ impl Action for RemoveColumn {
123 .collect();
135 .collect();
137+ let maybe_null_check = if !column.nullable {
138+ // Replace NOT NULL constraint with a constraint trigger that only triggers on the old schema.
139+ // 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.
141+ let query = format!(
143+ CREATE OR REPLACE FUNCTION {trigger_name}()
144+ RETURNS TRIGGER AS $$
146+ IF NOT reshape.is_new_schema() THEN
147+ IF NEW.{column} IS NULL THEN
148+ RAISE EXCEPTION '{column} can not be null';
153+ $$ language 'plpgsql';
155+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{table}";
157+ CREATE CONSTRAINT TRIGGER "{trigger_name}"
158+ AFTER INSERT OR UPDATE
161+ EXECUTE PROCEDURE {trigger_name}();
164+ trigger_name = self.null_constraint_trigger_name(ctx),
165+ column = self.column,
168+ .context("failed to create null constraint trigger")?;
173+ ALTER COLUMN {column}
177+ column = self.column
179+ .context("failed to remove column not null constraint")?;
183+ IF {value} IS NULL THEN
184+ RAISE EXCEPTION '{column_name} can not be null';
187+ column_name = self.column,
125 // TODO: If column is NOT NULL, remove the constraint and perform a NULL check in some other way.
193 // TODO: If column is NOT NULL, remove the constraint and perform a NULL check in some other way.
126 // Otherwise, it's not possible to update the NOT NULL column from another table even in the same transaction.
194 // Otherwise, it's not possible to update the NOT NULL column from another table even in the same transaction.
127 // Either make the NULL check in here or maybe use a constraint trigger: https://www.postgresql.org/docs/9.0/sql-createconstraint.html.
195 // Either make the NULL check in here or maybe use a constraint trigger: https://www.postgresql.org/docs/9.0/sql-createconstraint.html.
@@ -135,6 +203,7 @@ impl Action for RemoveColumn {
136 {declarations}
204 {declarations}
138 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
207 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
139 SET "{column_name}" = {value}
208 SET "{column_name}" = {value}
140 WHERE {where};
209 WHERE {where};
@@ -157,6 +226,8 @@ impl Action for RemoveColumn {
229+ if !column.nullable {}
@@ -194,10 +265,14 @@ impl Action for RemoveColumn {
195 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
266 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
196 DROP FUNCTION IF EXISTS "{trigger_name}";
267 DROP FUNCTION IF EXISTS "{trigger_name}";
269+ DROP TRIGGER IF EXISTS "{null_trigger_name}" ON "{table}";
270+ DROP FUNCTION IF EXISTS "{null_trigger_name}";
198 table = self.table,
272 table = self.table,
199 column = self.column,
273 column = self.column,
200 trigger_name = self.trigger_name(ctx),
274 trigger_name = self.trigger_name(ctx),
275+ null_trigger_name = self.null_constraint_trigger_name(ctx),
202 db.run(&query)
277 db.run(&query)
203 .context("failed to drop column and down trigger")?;
278 .context("failed to drop column and down trigger")?;
@@ -214,6 +289,18 @@ impl Action for RemoveColumn {
216 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
291 fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
292+ // We might have removed the NOT NULL check so we reinstate it
296+ ALTER COLUMN {column}
300+ column = self.column
302+ .context("failed to reinstate column not null constraint")?;
217 // Remove function and trigger
304 // Remove function and trigger
218 let trigger_table = match &self.down {
305 let trigger_table = match &self.down {
219 Some(Transformation::Update {
306 Some(Transformation::Update {
@@ -227,8 +314,13 @@ impl Action for RemoveColumn {
228 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
315 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
229 DROP FUNCTION IF EXISTS "{trigger_name}";
316 DROP FUNCTION IF EXISTS "{trigger_name}";
318+ DROP TRIGGER IF EXISTS "{null_trigger_name}" ON "{table}";
319+ DROP FUNCTION IF EXISTS "{null_trigger_name}";
231 trigger_name = self.trigger_name(ctx),
322 trigger_name = self.trigger_name(ctx),
323+ null_trigger_name = self.null_constraint_trigger_name(ctx),
233 .context("failed to drop down trigger")?;
325 .context("failed to drop down trigger")?;