@@ -1,6 +1,9 @@
1+use std::collections::HashMap;
1use super::{common::ForeignKey, Action, Column, MigrationContext};
3use super::{common::ForeignKey, Action, Column, MigrationContext};
2use crate::{
4use crate::{
3 db::{Conn, Transaction},
5 db::{Conn, Transaction},
4 schema::Schema,
7 schema::Schema,
6use anyhow::Context;
9use anyhow::Context;
@@ -14,6 +17,21 @@ pub struct CreateTable {
15 #[serde(default)]
18 #[serde(default)]
16 pub foreign_keys: Vec<ForeignKey>,
19 pub foreign_keys: Vec<ForeignKey>,
21+ pub up: Option<Transformation>,
24+#[derive(Serialize, Deserialize, Debug)]
25+pub struct Transformation {
27+ values: HashMap<String, String>,
28+ upsert_constraint: Option<String>,
32+ fn trigger_name(&self, ctx: &MigrationContext) -> String {
33+ format!("{}_create_table_{}", ctx.prefix(), self.name)
19#[typetag::serde(name = "create_table")]
37#[typetag::serde(name = "create_table")]
@@ -24,7 +42,7 @@ impl Action for CreateTable {
27− _ctx: &MigrationContext,
45+ ctx: &MigrationContext,
28 db: &mut dyn Conn,
46 db: &mut dyn Conn,
29 schema: &Schema,
47 schema: &Schema,
30 ) -> anyhow::Result<()> {
48 ) -> anyhow::Result<()> {
@@ -85,7 +103,7 @@ impl Action for CreateTable {
88− db.run(&format!(
106+ let query = &format!(
90 CREATE TABLE "{name}" (
108 CREATE TABLE "{name}" (
91 {definition}
109 {definition}
@@ -93,23 +111,118 @@ impl Action for CreateTable {
94 name = self.name,
112 name = self.name,
95 definition = definition_rows.join(",\n"),
113 definition = definition_rows.join(",\n"),
115+ db.run(query).context("failed to create table")?;
117+ if let Some(Transformation {
123+ let from_table = schema.get_table(db, &from_table)?;
125+ let declarations: Vec<String> = from_table
130+ "{alias} public.{table}.{real_name}%TYPE := NEW.{real_name};",
131+ table = from_table.real_name,
132+ alias = column.name,
133+ real_name = column.real_name,
138+ let (insert_columns, insert_values): (Vec<&str>, Vec<&str>) = values
140+ .map(|(k, v)| -> (&str, &str) { (k, v) }) // Force &String to &str
143+ let update_set: Vec<String> = values
145+ .map(|(field, value)| format!("\"{field}\" = {value}"))
148+ // Constraint to check for conflicts. Defaults to the primary key constraint.
149+ let conflict_constraint_name = match upsert_constraint {
150+ Some(custom_constraint) => custom_constraint.clone(),
151+ _ => format!("{table}_pkey", table = self.name),
154+ // Add triggers to fill in values as they are inserted/updated
155+ let query = format!(
157+ CREATE OR REPLACE FUNCTION {trigger_name}()
158+ RETURNS TRIGGER AS $$
159+ #variable_conflict use_variable
161+ IF NOT reshape.is_new_schema() THEN
165+ INSERT INTO public."{changed_table_real}" ({columns})
167+ ON CONFLICT ON CONSTRAINT "{conflict_constraint_name}"
174+ $$ language 'plpgsql';
176+ DROP TRIGGER IF EXISTS "{trigger_name}" ON "{from_table_real}";
177+ CREATE TRIGGER "{trigger_name}" BEFORE UPDATE OR INSERT ON "{from_table_real}" FOR EACH ROW EXECUTE PROCEDURE {trigger_name}();
179+ changed_table_real = self.name,
180+ from_table_real = from_table.real_name,
181+ trigger_name = self.trigger_name(ctx),
182+ declarations = declarations.join("\n"),
183+ columns = insert_columns.join(", "),
184+ values = insert_values.join(", "),
185+ updates = update_set.join(",\n"),
97− .context("failed to create table")?;
187+ db.run(&query).context("failed to create up trigger")?;
189+ // Backfill values in batches by touching the from table
190+ common::batch_touch_rows(db, &from_table.real_name, None)
191+ .context("failed to batch update existing rows")?;
101 fn complete<'a>(
197 fn complete<'a>(
103− _ctx: &MigrationContext,
199+ ctx: &MigrationContext,
104− _db: &'a mut dyn Conn,
200+ db: &'a mut dyn Conn,
105 ) -> anyhow::Result<Option<Transaction<'a>>> {
201 ) -> anyhow::Result<Option<Transaction<'a>>> {
106− // Do nothing
202+ // Remove triggers and procedures
203+ let query = format!(
205+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
207+ trigger_name = self.trigger_name(ctx),
209+ db.run(&query).context("failed to drop up trigger")?;
110 fn update_schema(&self, _ctx: &MigrationContext, _schema: &mut Schema) {}
214 fn update_schema(&self, _ctx: &MigrationContext, _schema: &mut Schema) {}
112− fn abort(&self, _ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
216+ fn abort(&self, ctx: &MigrationContext, db: &mut dyn Conn) -> anyhow::Result<()> {
217+ // Remove triggers and procedures
218+ let query = format!(
220+ DROP FUNCTION IF EXISTS "{trigger_name}" CASCADE;
222+ trigger_name = self.trigger_name(ctx),
224+ db.run(&query).context("failed to drop up trigger")?;
113 db.run(&format!(
226 db.run(&format!(
115 DROP TABLE IF EXISTS "{name}"
228 DROP TABLE IF EXISTS "{name}"