@@ -3,7 +3,7 @@ use crate::{
33 db::{Conn, Transaction},
44 schema::Schema,
55};
6−use anyhow::Context;
6+use anyhow::{bail, Context};
77use serde::{Deserialize, Serialize};
88
99#[derive(Serialize, Deserialize, Debug)]
@@ -43,6 +43,15 @@ impl AddColumn {
4343 )
4444 }
4545
46+ fn reverse_trigger_name(&self, ctx: &MigrationContext) -> String {
47+ format!(
48+ "{}_add_column_{}_{}_rev",
49+ ctx.prefix(),
50+ self.table,
51+ self.column.name
52+ )
53+ }
54+
4655 fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
4756 format!(
4857 "{}_add_column_not_null_{}_{}",
@@ -97,21 +106,21 @@ impl Action for AddColumn {
97106 );
98107 db.run(&query).context("failed to add column")?;
99108
109+ let declarations: Vec<String> = table
110+ .columns
111+ .iter()
112+ .map(|column| {
113+ format!(
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,
118+ )
119+ })
120+ .collect();
121+
100122 if let Some(up) = &self.up {
101123 if let Transformation::Simple(up) = up {
102− let declarations: Vec<String> = table
103− .columns
104− .iter()
105− .map(|column| {
106− format!(
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,
111− )
112− })
113− .collect();
114−
115124 // Add triggers to fill in values as they are inserted/updated
116125 let query = format!(
117126 r#"
@@ -151,15 +160,20 @@ impl Action for AddColumn {
151160 r#where,
152161 } = up
153162 {
163+ let existing_schema_name = match &ctx.existing_schema_name {
164+ Some(name) => name,
165+ None => bail!("can't use update without previous migration"),
166+ };
167+
154168 let from_table = schema.get_table(db, &from_table)?;
155169
156− let declarations: Vec<String> = from_table
170+ let from_table_assignments: Vec<String> = from_table
157171 .columns
158172 .iter()
159173 .map(|column| {
160174 format!(
161− "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
162− table = from_table.real_name,
175+ "{table}.{alias} = NEW.{real_name};",
176+ table = from_table.name,
163177 alias = column.name,
164178 real_name = column.real_name,
165179 )
@@ -175,11 +189,18 @@ impl Action for AddColumn {
175189 BEGIN
176190 IF NOT reshape.is_new_schema() THEN
177191 DECLARE
178− {declarations}
192+ {from_table} migration_{existing_schema_name}.{from_table}%ROWTYPE;
179193 BEGIN
194+ {assignments}
195+
196+ -- Don't trigger reverse trigger when making this update
197+ perform set_config('reshape.disable_triggers', 'TRUE', TRUE);
198+
180199 UPDATE public."{changed_table_real}"
181200 SET "{temp_column_name}" = {value}
182201 WHERE {where};
202+
203+ perform set_config('reshape.disable_triggers', '', TRUE);
183204 END;
184205 END IF;
185206 RETURN NEW;
@@ -189,14 +210,81 @@ impl Action for AddColumn {
189210 DROP TRIGGER IF EXISTS "{trigger_name}" ON "{from_table_real}";
190211 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
191212 "#,
213+ assignments = from_table_assignments.join("\n"),
192214 changed_table_real = table.real_name,
215+ from_table = from_table.name,
193216 from_table_real = from_table.real_name,
194217 trigger_name = self.trigger_name(ctx),
195− declarations = declarations.join("\n"),
218+ // declarations = from_table_declarations.join("\n"),
196219 temp_column_name = temp_column_name,
197220 );
198221 db.run(&query).context("failed to create up trigger")?;
199222
223+ let from_table_columns = from_table
224+ .columns
225+ .iter()
226+ .map(|column| format!("{} as {}", column.real_name, column.name))
227+ .collect::<Vec<String>>()
228+ .join(", ");
229+
230+ let changed_table_assignments: Vec<String> = table
231+ .columns
232+ .iter()
233+ .map(|column| {
234+ format!(
235+ "{table}.{alias} := NEW.{real_name};",
236+ table = table.name,
237+ alias = column.name,
238+ real_name = column.real_name,
239+ )
240+ })
241+ .collect();
242+
243+ // Add triggers to fill in values as they are inserted/updated
244+ let query = format!(
245+ r#"
246+ CREATE OR REPLACE FUNCTION {trigger_name}()
247+ RETURNS TRIGGER AS $$
248+ #variable_conflict use_variable
249+ BEGIN
250+ IF NOT reshape.is_new_schema() AND NOT current_setting('reshape.disable_triggers', TRUE) = 'TRUE' THEN
251+ DECLARE
252+ {changed_table} migration_{existing_schema_name}.{changed_table}%ROWTYPE;
253+ __temp_row migration_{existing_schema_name}.{from_table}%ROWTYPE;
254+ BEGIN
255+ {changed_table_assignments}
256+
257+ SELECT {from_table_columns}
258+ INTO __temp_row
259+ FROM migration_{existing_schema_name}.{from_table} {from_table}
260+ WHERE {where};
261+
262+ DECLARE
263+ {from_table} migration_{existing_schema_name}.{from_table}%ROWTYPE;
264+ BEGIN
265+ {from_table} = __temp_row;
266+ NEW.{temp_column_name} = {value};
267+ END;
268+ END;
269+ END IF;
270+ RETURN NEW;
271+ END
272+ $$ language 'plpgsql';
273+
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}();
276+ "#,
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"),
284+ );
285+ db.run(&query)
286+ .context("failed to create reverse up trigger")?;
287+
200288 // Backfill values in batches by touching the from table
201289 common::batch_touch_rows(db, &from_table.real_name, None)
202290 .context("failed to batch update existing rows")?;
@@ -210,10 +298,10 @@ impl Action for AddColumn {
210298 if !self.column.nullable {
211299 let query = format!(
212300 r#"
213− ALTER TABLE "{table}"
214− ADD CONSTRAINT "{constraint_name}"
215− CHECK ("{column}" IS NOT NULL) NOT VALID
216− "#,
301+ ALTER TABLE "{table}"
302+ ADD CONSTRAINT "{constraint_name}"
303+ CHECK ("{column}" IS NOT NULL) NOT VALID
304+ "#,
217305 table = self.table,
218306 constraint_name = self.not_null_constraint_name(ctx),
219307 column = temp_column_name,
@@ -236,8 +324,10 @@ impl Action for AddColumn {
236324 let query = format!(
237325 r#"
238326 DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
327+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
239328 "#,
240329 trigger_name = self.trigger_name(ctx),
330+ reverse_trigger_name = self.reverse_trigger_name(ctx),
241331 );
242332 transaction
243333 .run(&query)
@@ -329,8 +419,10 @@ impl Action for AddColumn {
329419 let query = format!(
330420 r#"
331421 DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
422+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
332423 "#,
333424 trigger_name = self.trigger_name(ctx),
425+ reverse_trigger_name = self.reverse_trigger_name(ctx),
334426 );
335427 db.run(&query).context("failed to drop up trigger")?;
336428