on main help sign in
Add reverse trigger for complex up and down tranformations
bf1fe13fabianlindfors
src/migrations/add_column.rs+115−23
@@ -3,7 +3,7 @@ use crate::{
3 db::{Conn, Transaction},
3 db::{Conn, Transaction},
4 schema::Schema,
4 schema::Schema,
5};
5};
6−use anyhow::Context;
6+use anyhow::{bail, Context};
7use serde::{Deserialize, Serialize};
7use serde::{Deserialize, Serialize};
8
8
9#[derive(Serialize, Deserialize, Debug)]
9#[derive(Serialize, Deserialize, Debug)]
@@ -43,6 +43,15 @@ impl AddColumn {
43 )
43 )
44 }
44 }
45
45
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+
46 fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
55 fn not_null_constraint_name(&self, ctx: &MigrationContext) -> String {
47 format!(
56 format!(
48 "{}_add_column_not_null_{}_{}",
57 "{}_add_column_not_null_{}_{}",
@@ -97,21 +106,21 @@ impl Action for AddColumn {
97 );
106 );
98 db.run(&query).context("failed to add column")?;
107 db.run(&query).context("failed to add column")?;
99
108
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+
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
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−
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!(
117 r#"
126 r#"
@@ -151,15 +160,20 @@ impl Action for AddColumn {
151 r#where,
160 r#where,
152 } = up
161 } = up
153 {
162 {
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+
154 let from_table = schema.get_table(db, &from_table)?;
168 let from_table = schema.get_table(db, &from_table)?;
155
169
156− let declarations: Vec<String> = from_table
170+ let from_table_assignments: Vec<String> = from_table
157 .columns
171 .columns
158 .iter()
172 .iter()
159 .map(|column| {
173 .map(|column| {
160 format!(
174 format!(
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,
165 )
179 )
@@ -175,11 +189,18 @@ impl Action for AddColumn {
175 BEGIN
189 BEGIN
176 IF NOT reshape.is_new_schema() THEN
190 IF NOT reshape.is_new_schema() THEN
177 DECLARE
191 DECLARE
178− {declarations}
192+ {from_table} migration_{existing_schema_name}.{from_table}%ROWTYPE;
179 BEGIN
193 BEGIN
194+ {assignments}
195+
196+ -- Don't trigger reverse trigger when making this update
197+ perform set_config('reshape.disable_triggers', 'TRUE', TRUE);
198+
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};
202+
203+ perform set_config('reshape.disable_triggers', '', TRUE);
183 END;
204 END;
184 END IF;
205 END IF;
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}();
191 "#,
212 "#,
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,
197 );
220 );
198 db.run(&query).context("failed to create up trigger")?;
221 db.run(&query).context("failed to create up trigger")?;
199
222
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+
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!(
212 r#"
300 r#"
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
216− "#,
304+ "#,
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!(
237 r#"
325 r#"
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;
239 "#,
328 "#,
240 trigger_name = self.trigger_name(ctx),
329 trigger_name = self.trigger_name(ctx),
330+ reverse_trigger_name = self.reverse_trigger_name(ctx),
241 );
331 );
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!(
330 r#"
420 r#"
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;
332 "#,
423 "#,
333 trigger_name = self.trigger_name(ctx),
424 trigger_name = self.trigger_name(ctx),
425+ reverse_trigger_name = self.reverse_trigger_name(ctx),
334 );
426 );
335 db.run(&query).context("failed to drop up trigger")?;
427 db.run(&query).context("failed to drop up trigger")?;
336
428
src/migrations/remove_column.rs+110−42
@@ -34,6 +34,15 @@ impl RemoveColumn {
34 )
34 )
35 }
35 }
36
36
37+ fn reverse_trigger_name(&self, ctx: &MigrationContext) -> String {
38+ format!(
39+ "{}_remove_column_{}_{}_rev",
40+ ctx.prefix(),
41+ self.table,
42+ self.column
43+ )
44+ }
45+
37 fn null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
46 fn null_constraint_trigger_name(&self, ctx: &MigrationContext) -> String {
38 format!(
47 format!(
39 "{}_remove_column_{}_{}_nn",
48 "{}_remove_column_{}_{}_nn",
@@ -121,19 +130,6 @@ impl Action for RemoveColumn {
121
130
122 let from_table = schema.get_table(db, &from_table)?;
131 let from_table = schema.get_table(db, &from_table)?;
123
132
124− let declarations: Vec<String> = from_table
125− .columns
126− .iter()
127− .map(|column| {
128− format!(
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,
133− )
134− })
135− .collect();
136−
137 let maybe_null_check = if !column.nullable {
133 let maybe_null_check = if !column.nullable {
138 // Replace NOT NULL constraint with a constraint trigger that only triggers on the old schema.
134 // 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.
135 // As we are using a complex down function, we must remove the NOT NULL check for the new schema.
@@ -190,6 +186,19 @@ impl Action for RemoveColumn {
190 "".to_string()
186 "".to_string()
191 };
187 };
192
188
189+ let into_variables = from_table
190+ .columns
191+ .iter()
192+ .map(|column| {
193+ format!(
194+ "NEW.{real_name} AS {alias}",
195+ alias = column.name,
196+ real_name = column.real_name,
197+ )
198+ })
199+ .collect::<Vec<String>>()
200+ .join(", ");
201+
193 let query = format!(
202 let query = format!(
194 r#"
203 r#"
195 CREATE OR REPLACE FUNCTION {trigger_name}()
204 CREATE OR REPLACE FUNCTION {trigger_name}()
@@ -198,12 +207,21 @@ impl Action for RemoveColumn {
198 BEGIN
207 BEGIN
199 IF reshape.is_new_schema() THEN
208 IF reshape.is_new_schema() THEN
200 DECLARE
209 DECLARE
201− {declarations}
210+ {from_table} record;
202 BEGIN
211 BEGIN
212+ SELECT {into_variables}
213+ INTO {from_table};
214+
203 {maybe_null_check}
215 {maybe_null_check}
216+
217+ -- Don't trigger reverse trigger when making this update
218+ perform set_config('reshape.disable_triggers', 'TRUE', TRUE);
219+
204 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
220 UPDATE "migration_{existing_schema_name}"."{changed_table}" "{changed_table}"
205 SET "{column_name}" = {value}
221 SET "{column_name}" = {value}
206 WHERE {where};
222 WHERE {where};
223+
224+ perform set_config('reshape.disable_triggers', '', TRUE);
207 END;
225 END;
208 END IF;
226 END IF;
209 RETURN NEW;
227 RETURN NEW;
@@ -214,12 +232,81 @@ impl Action for RemoveColumn {
214 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
232 CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
215 "#,
233 "#,
216 changed_table = self.table,
234 changed_table = self.table,
235+ from_table = from_table.name,
217 from_table_real = from_table.real_name,
236 from_table_real = from_table.real_name,
218 column_name = self.column,
237 column_name = self.column,
219 trigger_name = self.trigger_name(ctx),
238 trigger_name = self.trigger_name(ctx),
220− declarations = declarations.join("\n"),
239+ // declarations = declarations.join("\n"),
221 );
240 );
222 db.run(&query).context("failed to create down trigger")?;
241 db.run(&query).context("failed to create down trigger")?;
242+
243+ let changed_into_variables = table
244+ .columns
245+ .iter()
246+ .map(|column| {
247+ format!(
248+ "NEW.{real_name} AS {alias}",
249+ alias = column.name,
250+ real_name = column.real_name,
251+ )
252+ })
253+ .collect::<Vec<String>>()
254+ .join(", ");
255+
256+ let from_table_columns = from_table
257+ .columns
258+ .iter()
259+ .map(|column| format!("{} as {}", column.real_name, column.name))
260+ .collect::<Vec<String>>()
261+ .join(", ");
262+
263+ let query = format!(
264+ r#"
265+ CREATE OR REPLACE FUNCTION {trigger_name}()
266+ RETURNS TRIGGER AS $$
267+ #variable_conflict use_variable
268+ BEGIN
269+ IF reshape.is_new_schema() AND NOT current_setting('reshape.disable_triggers', TRUE) = 'TRUE' THEN
270+ DECLARE
271+ {changed_table} record;
272+ __temp_row record;
273+ BEGIN
274+ SELECT {changed_into_variables}
275+ INTO {changed_table};
276+
277+ SELECT *
278+ INTO __temp_row
279+ FROM (
280+ SELECT {from_table_columns}
281+ FROM public.{from_table_real}
282+ ) {from_table}
283+ WHERE {where};
284+
285+ DECLARE
286+ {from_table} record;
287+ BEGIN
288+ {from_table} := __temp_row;
289+ NEW.{column_name_real} = {value};
290+ END;
291+ END;
292+ END IF;
293+ RETURN NEW;
294+ END
295+ $$ language 'plpgsql';
296+
297+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{changed_table_real}";
298+ CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{changed_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
299+ "#,
300+ changed_table = table.name,
301+ changed_table_real = table.real_name,
302+ from_table = from_table.name,
303+ from_table_real = from_table.real_name,
304+ column_name_real = column.real_name,
305+ trigger_name = self.reverse_trigger_name(ctx),
306+ // declarations = declarations.join("\n"),
307+ );
308+ db.run(&query)
309+ .context("failed to create reverse down trigger")?;
223 }
310 }
224 }
311 }
225
312
@@ -247,28 +334,19 @@ impl Action for RemoveColumn {
247 }
334 }
248
335
249 // Remove column, function and trigger
336 // Remove column, function and trigger
250− let trigger_table = match &self.down {
251− Some(Transformation::Update {
252− table,
253− value: _,
254− r#where: _,
255− }) => table,
256− _ => &self.table,
257− };
258 let query = format!(
337 let query = format!(
259 r#"
338 r#"
260 ALTER TABLE "{table}"
339 ALTER TABLE "{table}"
261 DROP COLUMN IF EXISTS "{column}";
340 DROP COLUMN IF EXISTS "{column}";
262
341
263− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
342+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
264− DROP FUNCTION IF EXISTS "{trigger_name}";
343+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
265−
266− DROP TRIGGER IF EXISTS "{null_trigger_name}" ON "{table}";
267− DROP FUNCTION IF EXISTS "{null_trigger_name}";
344+ DROP FUNCTION IF EXISTS "{null_trigger_name}" CASCADE;
268 "#,
345 "#,
269 table = self.table,
346 table = self.table,
270 column = self.column,
347 column = self.column,
271 trigger_name = self.trigger_name(ctx),
348 trigger_name = self.trigger_name(ctx),
349+ reverse_trigger_name = self.reverse_trigger_name(ctx),
272 null_trigger_name = self.null_constraint_trigger_name(ctx),
350 null_trigger_name = self.null_constraint_trigger_name(ctx),
273 );
351 );
274 db.run(&query)
352 db.run(&query)
@@ -299,24 +377,14 @@ impl Action for RemoveColumn {
299 .context("failed to reinstate column not null constraint")?;
377 .context("failed to reinstate column not null constraint")?;
300
378
301 // Remove function and trigger
379 // Remove function and trigger
302− let trigger_table = match &self.down {
303− Some(Transformation::Update {
304− table,
305− value: _,
306− r#where: _,
307− }) => table,
308− _ => &self.table,
309− };
310 db.run(&format!(
380 db.run(&format!(
311 r#"
381 r#"
312− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{trigger_table}";
382+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
313− DROP FUNCTION IF EXISTS "{trigger_name}";
383+ DROP FUNCTION IF EXISTS "{reverse_trigger_name}" CASCADE;
314−
315− DROP TRIGGER IF EXISTS "{null_trigger_name}" ON "{table}";
316− DROP FUNCTION IF EXISTS "{null_trigger_name}";
384+ DROP FUNCTION IF EXISTS "{null_trigger_name}" CASCADE;
317 "#,
385 "#,
318− table = self.table,
319 trigger_name = self.trigger_name(ctx),
386 trigger_name = self.trigger_name(ctx),
387+ reverse_trigger_name = self.reverse_trigger_name(ctx),
320 null_trigger_name = self.null_constraint_trigger_name(ctx),
388 null_trigger_name = self.null_constraint_trigger_name(ctx),
321 ))
389 ))
322 .context("failed to drop down trigger")?;
390 .context("failed to drop down trigger")?;
tests/complex.rs+164−5
@@ -1,6 +1,165 @@
1mod common;
1mod common;
2use common::Test;
2use common::Test;
3
3
4+#[test]
5+fn move_column_between_tables() {
6+ let mut test = Test::new("Move column between tables");
7+
8+ test.first_migration(
9+ r#"
10+ name = "create_tables"
11+
12+ [[actions]]
13+ type = "create_table"
14+ name = "users"
15+ primary_key = ["id"]
16+
17+ [[actions.columns]]
18+ name = "id"
19+ type = "INTEGER"
20+
21+ [[actions.columns]]
22+ name = "email"
23+ type = "TEXT"
24+
25+ [[actions]]
26+ type = "create_table"
27+ name = "profiles"
28+ primary_key = ["id"]
29+
30+ [[actions.columns]]
31+ name = "id"
32+ type = "INTEGER"
33+
34+ [[actions.columns]]
35+ name = "user_id"
36+ type = "INTEGER"
37+ nullable = false
38+ "#,
39+ );
40+
41+ test.second_migration(
42+ r#"
43+ name = "move_email_column"
44+
45+ [[actions]]
46+ type = "add_column"
47+ table = "profiles"
48+
49+ [actions.column]
50+ name = "email"
51+ type = "TEXT"
52+ nullable = false
53+
54+ # When `users` is updated in the old schema, we write the email value to `profiles`
55+ # When `profiles` is updated in the old schema, we touch the equivalent `users` row to update it
56+ [actions.up]
57+ table = "users"
58+ value = "users.email"
59+ where = "profiles.user_id = users.id"
60+
61+ [[actions]]
62+ type = "remove_column"
63+ table = "users"
64+ column = "email"
65+
66+ # When `profiles` is changed in the new schema, we write the email address back to the removed column
67+ [actions.down]
68+ table = "profiles"
69+ value = "profiles.email"
70+ where = "users.id = profiles.user_id"
71+ "#,
72+ );
73+
74+ test.after_first(|db| {
75+ db.simple_query("INSERT INTO users (id, email) VALUES (1, 'test1@test.com')")
76+ .unwrap();
77+ db.simple_query("INSERT INTO users (id, email) VALUES (2, 'test2@test.com')")
78+ .unwrap();
79+
80+ db.simple_query("INSERT INTO profiles (id, user_id) VALUES (1, 1)")
81+ .unwrap();
82+ db.simple_query("INSERT INTO profiles (id, user_id) VALUES (2, 2)")
83+ .unwrap();
84+ });
85+
86+ test.intermediate(|old_db, new_db| {
87+ // Ensure emails were backfilled into profiles
88+ let profiles_emails: Vec<String> = new_db
89+ .query(
90+ r#"
91+ SELECT email
92+ FROM profiles
93+ ORDER BY id
94+ "#,
95+ &[],
96+ )
97+ .unwrap()
98+ .iter()
99+ .map(|row| row.get("email"))
100+ .collect();
101+ assert_eq!(vec!("test1@test.com", "test2@test.com"), profiles_emails);
102+
103+ // Ensure insert in old schema updates new
104+ old_db
105+ .simple_query("INSERT INTO users (id, email) VALUES (3, 'test3@test.com')")
106+ .unwrap();
107+ old_db
108+ .simple_query("INSERT INTO profiles (id, user_id) VALUES (3, 3)")
109+ .unwrap();
110+ let new_email: String = new_db
111+ .query("SELECT email FROM profiles WHERE id = 3", &[])
112+ .unwrap()
113+ .first()
114+ .map(|row| row.get("email"))
115+ .unwrap();
116+ assert_eq!("test3@test.com", new_email);
117+
118+ // Ensure updates in old schema updates new
119+ old_db
120+ .simple_query("UPDATE users SET email = 'test3+updated@test.com' WHERE id = 3")
121+ .unwrap();
122+ let new_email: String = new_db
123+ .query("SELECT email FROM profiles WHERE id = 3", &[])
124+ .unwrap()
125+ .first()
126+ .map(|row| row.get("email"))
127+ .unwrap();
128+ assert_eq!("test3+updated@test.com", new_email);
129+
130+ // Ensure insert in new schema updates old
131+ new_db
132+ .simple_query(
133+ "INSERT INTO profiles (id, user_id, email) VALUES (4, 4, 'test4@test.com')",
134+ )
135+ .unwrap();
136+ new_db
137+ .simple_query("INSERT INTO users (id) VALUES (4)")
138+ .unwrap();
139+ let old_email: String = old_db
140+ .query("SELECT email FROM users WHERE id = 4", &[])
141+ .unwrap()
142+ .first()
143+ .map(|row| row.get("email"))
144+ .unwrap();
145+ assert_eq!("test4@test.com", old_email);
146+
147+ // Ensure update in new schema updates old
148+ new_db
149+ .simple_query("UPDATE profiles SET email = 'test4+updated@test.com' WHERE id = 4")
150+ .unwrap();
151+ let old_email: String = old_db
152+ .query("SELECT email FROM users WHERE id = 4", &[])
153+ .unwrap()
154+ .first()
155+ .map(|row| row.get("email"))
156+ .unwrap();
157+ assert_eq!("test4+updated@test.com", old_email);
158+ });
159+
160+ test.run();
161+}
162+
4#[test]
163#[test]
5fn extract_relation_into_new_table() {
164fn extract_relation_into_new_table() {
6 let mut test = Test::new("Extract relation into new table");
165 let mut test = Test::new("Extract relation into new table");
@@ -64,7 +223,7 @@ fn extract_relation_into_new_table() {
64 [actions.up]
223 [actions.up]
65 table = "users"
224 table = "users"
66 values = { user_id = "id", account_id = "account_id", role = "UPPER(account_role)" }
225 values = { user_id = "id", account_id = "account_id", role = "UPPER(account_role)" }
67− where = "user_id = id"
226+ where = "user_account_connections.user_id = users.id"
68
227
69 [[actions]]
228 [[actions]]
70 type = "remove_column"
229 type = "remove_column"
@@ -73,8 +232,8 @@ fn extract_relation_into_new_table() {
73
232
74 [actions.down]
233 [actions.down]
75 table = "user_account_connections"
234 table = "user_account_connections"
76− value = "account_id"
235+ value = "user_account_connections.account_id"
77− where = "id = user_id"
236+ where = "users.id = user_account_connections.user_id"
78
237
79 [[actions]]
238 [[actions]]
80 type = "remove_column"
239 type = "remove_column"
@@ -83,8 +242,8 @@ fn extract_relation_into_new_table() {
83
242
84 [actions.down]
243 [actions.down]
85 table = "user_account_connections"
244 table = "user_account_connections"
86− value = "LOWER(role)"
245+ value = "LOWER(user_account_connections.role)"
87− where = "id = user_id"
246+ where = "users.id = user_account_connections.user_id"
88 "#,
247 "#,
89 );
248 );
90
249