@@ -9,8 +9,19 @@ use serde::{Deserialize, Serialize};
9#[derive(Serialize, Deserialize, Debug)]
9#[derive(Serialize, Deserialize, Debug)]
10pub struct AddColumn {
10pub struct AddColumn {
11 pub table: String,
11 pub table: String,
12− pub up: Option<String>,
13 pub column: Column,
12 pub column: Column,
13+ pub up: Option<Transformation>,
16+#[derive(Serialize, Deserialize, Debug)]
18+pub enum Transformation {
16impl AddColumn {
27impl AddColumn {
@@ -87,52 +98,109 @@ impl Action for AddColumn {
87 db.run(&query).context("failed to add column")?;
98 db.run(&query).context("failed to add column")?;
89 if let Some(up) = &self.up {
100 if let Some(up) = &self.up {
101+ if let Transformation::Simple(up) = up {
90− let declarations: Vec<String> = table
102+ let declarations: Vec<String> = table
91− .columns
103+ .columns
93− .map(|column| {
105+ .map(|column| {
94− format!(
106+ format!(
95− "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
107+ "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
96− table = table.real_name,
108+ table = table.real_name,
97− alias = column.name,
109+ alias = column.name,
98− real_name = column.real_name,
110+ real_name = column.real_name,
101− .collect();
113+ .collect();
103− // Add triggers to fill in values as they are inserted/updated
104− let query = format!(
106− CREATE OR REPLACE FUNCTION {trigger_name}()
107− RETURNS TRIGGER AS $$
109− IF NOT reshape.is_new_schema() THEN
113− NEW.{temp_column_name} = {up};
118− $$ language 'plpgsql';
120− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{table}";
121− CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{table}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
123− temp_column_name = temp_column_name,
124− trigger_name = self.trigger_name(ctx),
127− declarations = declarations.join("\n"),
129− db.run(&query).context("failed to create up trigger")?;
115+ // Add triggers to fill in values as they are inserted/updated
116+ let query = format!(
118+ CREATE OR REPLACE FUNCTION {trigger_name}()
119+ RETURNS TRIGGER AS $$
121+ IF NOT reshape.is_new_schema() THEN
125+ NEW.{temp_column_name} = {up};
130+ $$ language 'plpgsql';
132+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{table}";
133+ CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{table}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
135+ temp_column_name = temp_column_name,
136+ trigger_name = self.trigger_name(ctx),
139+ declarations = declarations.join("\n"),
141+ db.run(&query).context("failed to create up trigger")?;
132− // Backfill values in batches
143+ // Backfill values in batches
133− if self.up.is_some() {
144+ common::batch_touch_rows(db, &table.real_name, Some(&temp_column_name))
145+ .context("failed to batch update existing rows")?;
148+ if let Transformation::Update {
154+ let from_table = schema.get_table(db, &from_table)?;
156+ let declarations: Vec<String> = from_table
161+ "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
162+ table = from_table.real_name,
163+ alias = column.name,
164+ real_name = column.real_name,
169+ // Add triggers to fill in values as they are inserted/updated
170+ let query = format!(
172+ CREATE OR REPLACE FUNCTION {trigger_name}()
173+ RETURNS TRIGGER AS $$
174+ #variable_conflict use_variable
176+ IF NOT reshape.is_new_schema() THEN
180+ UPDATE public."{changed_table_real}"
181+ SET "{temp_column_name}" = {value}
187+ $$ language 'plpgsql';
189+ 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}();
192+ changed_table_real = table.real_name,
193+ from_table_real = from_table.real_name,
194+ trigger_name = self.trigger_name(ctx),
195+ declarations = declarations.join("\n"),
196+ temp_column_name = temp_column_name,
198+ db.run(&query).context("failed to create up trigger")?;
200+ // Backfill values in batches by touching the from table
134− common::batch_touch_rows(db, &table.real_name, &temp_column_name)
201+ common::batch_touch_rows(db, &from_table.real_name, None)
135− .context("failed to batch update existing rows")?;
202+ .context("failed to batch update existing rows")?;
138 // Add a temporary NOT NULL constraint if the column shouldn't be nullable.
206 // Add a temporary NOT NULL constraint if the column shouldn't be nullable.
@@ -167,10 +235,8 @@ impl Action for AddColumn {
167 // Remove triggers and procedures
235 // Remove triggers and procedures
168 let query = format!(
236 let query = format!(
170− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{table}";
171− DROP FUNCTION IF EXISTS "{trigger_name}";
238+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
174 trigger_name = self.trigger_name(ctx),
240 trigger_name = self.trigger_name(ctx),
176 transaction
242 transaction
@@ -262,10 +328,8 @@ impl Action for AddColumn {
262 // Remove triggers and procedures
328 // Remove triggers and procedures
263 let query = format!(
329 let query = format!(
265− DROP TRIGGER IF EXISTS "{trigger_name}" ON "{table}";
266− DROP FUNCTION IF EXISTS "{trigger_name}";
331+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
269 trigger_name = self.trigger_name(ctx),
333 trigger_name = self.trigger_name(ctx),
271 db.run(&query).context("failed to drop up trigger")?;
335 db.run(&query).context("failed to drop up trigger")?;