@@ -3,7 +3,7 @@ use crate::{
3 db::{Conn, Transaction},
3 db::{Conn, Transaction},
4 schema::Schema,
4 schema::Schema,
6−use anyhow::Context;
6+use anyhow::{bail, Context};
7use serde::{Deserialize, Serialize};
7use serde::{Deserialize, Serialize};
9#[derive(Serialize, Deserialize, Debug)]
9#[derive(Serialize, Deserialize, Debug)]
@@ -43,6 +43,15 @@ impl AddColumn {
46+ fn reverse_trigger_name(&self, ctx: &MigrationContext) -> String {
48+ "{}_add_column_{}_{}_rev",
46 fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
55 fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
48 "{}_add_column_not_null_{}_{}",
57 "{}_add_column_not_null_{}_{}",
@@ -97,21 +106,21 @@ impl Action for AddColumn {
98 db.run(&query).context("failed to add column")?;
107 db.run(&query).context("failed to add column")?;
109+ let declarations: Vec<String> = table
114+ "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
115+ table = table.real_name,
116+ alias = column.name,
117+ real_name = column.real_name,
100 if let Some(up) = &self.up {
122 if let Some(up) = &self.up {
101 if let Transformation::Simple(up) = up {
123 if let Transformation::Simple(up) = up {
102− let declarations: Vec<String> = table
107− "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
108− table = table.real_name,
109− alias = column.name,
110− real_name = column.real_name,
115 // Add triggers to fill in values as they are inserted/updated
124 // Add triggers to fill in values as they are inserted/updated
116 let query = format!(
125 let query = format!(
@@ -151,15 +160,20 @@ impl Action for AddColumn {
163+ let existing_schema_name = match &ctx.existing_schema_name {
165+ None => bail!("can't use update without previous migration"),
154 let from_table = schema.get_table(db, &from_table)?;
168 let from_table = schema.get_table(db, &from_table)?;
156− let declarations: Vec<String> = from_table
170+ let from_table_assignments: Vec<String> = from_table
159 .map(|column| {
173 .map(|column| {
161− "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
175+ "{table}.{alias} = NEW.{real_name};",
162− table = from_table.real_name,
176+ table = from_table.name,
163 alias = column.name,
177 alias = column.name,
164 real_name = column.real_name,
178 real_name = column.real_name,
@@ -175,11 +189,18 @@ impl Action for AddColumn {
176 IF NOT reshape.is_new_schema() THEN
190 IF NOT reshape.is_new_schema() THEN
178− {declarations}
192+ {from_table} migration_{existing_schema_name}.{from_table}%ROWTYPE;
196+ -- Don't trigger reverse trigger when making this update
197+ perform set_config('reshape.disable_triggers', 'TRUE', TRUE);
180 UPDATE public."{changed_table_real}"
199 UPDATE public."{changed_table_real}"
181 SET "{temp_column_name}" = {value}
200 SET "{temp_column_name}" = {value}
182 WHERE {where};
201 WHERE {where};
203+ perform set_config('reshape.disable_triggers', '', TRUE);
185 RETURN NEW;
206 RETURN NEW;
@@ -189,14 +210,81 @@ impl Action for AddColumn {
189 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{from_table_real}";
210 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{from_table_real}";
190 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
211 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
213+ assignments = from_table_assignments.join("\n"),
192 changed_table_real = table.real_name,
214 changed_table_real = table.real_name,
215+ from_table = from_table.name,
193 from_table_real = from_table.real_name,
216 from_table_real = from_table.real_name,
194 trigger_name = self.trigger_name(ctx),
217 trigger_name = self.trigger_name(ctx),
195− declarations = declarations.join("\n"),
218+ // declarations = from_table_declarations.join("\n"),
196 temp_column_name = temp_column_name,
219 temp_column_name = temp_column_name,
198 db.run(&query).context("failed to create up trigger")?;
221 db.run(&query).context("failed to create up trigger")?;
223+ let from_table_columns = from_table
226+ .map(|column| format!("{} as {}", column.real_name, column.name))
227+ .collect::<Vec<String>>()
230+ let changed_table_assignments: Vec<String> = table
235+ "{table}.{alias} := NEW.{real_name};",
237+ alias = column.name,
238+ real_name = column.real_name,
243+ // Add triggers to fill in values as they are inserted/updated
244+ let query = format!(
246+ CREATE OR REPLACE FUNCTION {trigger_name}()
247+ RETURNS TRIGGER AS $$
248+ #variable_conflict use_variable
250+ IF NOT reshape.is_new_schema() AND NOT current_setting('reshape.disable_triggers', TRUE) = 'TRUE' THEN
252+ {changed_table} migration_{existing_schema_name}.{changed_table}%ROWTYPE;
253+ __temp_row migration_{existing_schema_name}.{from_table}%ROWTYPE;
255+ {changed_table_assignments}
257+ SELECT {from_table_columns}
259+ FROM migration_{existing_schema_name}.{from_table} {from_table}
263+ {from_table} migration_{existing_schema_name}.{from_table}%ROWTYPE;
265+ {from_table} = __temp_row;
266+ NEW.{temp_column_name} = {value};
272+ $$ language 'plpgsql';
274+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{changed_table_real}";
275+ CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{changed_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
277+ changed_table_assignments = changed_table_assignments.join("\n"),
278+ changed_table_real = table.real_name,
279+ changed_table = table.name,
280+ from_table = from_table.name,
281+ trigger_name = self.reverse_trigger_name(ctx),
282+ temp_column_name = temp_column_name,
283+ // declarations = declarations.join("\n"),
286+ .context("failed to create reverse up trigger")?;
200 // Backfill values in batches by touching the from table
288 // Backfill values in batches by touching the from table
201 common::batch_touch_rows(db, &from_table.real_name, None)
289 common::batch_touch_rows(db, &from_table.real_name, None)
202 .context("failed to batch update existing rows")?;
290 .context("failed to batch update existing rows")?;
@@ -210,10 +298,10 @@ impl Action for AddColumn {
210 if !self.column.nullable {
298 if !self.column.nullable {
211 let query = format!(
299 let query = format!(
213− ALTER TABLE "{table}"
301+ ALTER TABLE "{table}"
214− ADD CONSTRAINT "{constraint_name}"
302+ ADD CONSTRAINT "{constraint_name}"
215− CHECK ("{column}" IS NOT NULL) NOT VALID
303+ CHECK ("{column}" IS NOT NULL) NOT VALID
217 table = self.table,
305 table = self.table,
218 constraint_name = self.not_null_constraint_name(ctx),
306 constraint_name = self.not_null_constraint_name(ctx),
219 column = temp_column_name,
307 column = temp_column_name,
@@ -236,8 +324,10 @@ impl Action for AddColumn {
236 let query = format!(
324 let query = format!(
238 DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
326 DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
327+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
240 trigger_name = self.trigger_name(ctx),
329 trigger_name = self.trigger_name(ctx),
330+ reverse_trigger_name = self.reverse_trigger_name(ctx),
242 transaction
332 transaction
243 .run(&query)
333 .run(&query)
@@ -329,8 +419,10 @@ impl Action for AddColumn {
329 let query = format!(
419 let query = format!(
331 DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
421 DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
422+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
333 trigger_name = self.trigger_name(ctx),
424 trigger_name = self.trigger_name(ctx),
425+ reverse_trigger_name = self.reverse_trigger_name(ctx),
335 db.run(&query).context("failed to drop up trigger")?;
427 db.run(&query).context("failed to drop up trigger")?;