Skip to content

Commit

Permalink
Upsert on save (#117)
Browse files Browse the repository at this point in the history
Save does upsert rather than creating conflicts, as long as the primary key is not auto.
Fixes #112
  • Loading branch information
Electron100 authored Jun 27, 2023
1 parent dcebd49 commit d15fe87
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 13 deletions.
18 changes: 18 additions & 0 deletions butane/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,21 @@ fn basic_load_first_ordered(conn: Connection) {
assert_eq!(found_desc, Some(foo2));
}
testall!(basic_load_first_ordered);

fn save_upserts_by_default(conn: Connection) {
let mut foo = Foo::new(1);
foo.bar = 42;
foo.save(&conn).unwrap();

// Create another foo object with the same primary key,
// but a different bar value.
let mut foo = Foo::new(1);
foo.bar = 43;
// Save should do an upsert, so it will update the bar value
// rather than throwing a conflict
foo.save(&conn).unwrap();

let retrieved = Foo::get(&conn, 1).unwrap();
assert_eq!(retrieved.bar, 43);
}
testall!(save_upserts_by_default);
10 changes: 9 additions & 1 deletion butane_core/src/codegen/dbobj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,25 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 {
#pklit,
<#pktype as butane::FieldType>::SQLTYPE);
if self.state.saved {
// Already exists in db, do an update
#(#values_no_pk)*
if values.len() > 0 {
conn.update(Self::TABLE,
pkcol,
butane::ToSql::to_sql_ref(self.pk()),
&[#save_cols], &values)?;
}
} else {
} else if #auto_pk {
// Since we expect our pk field to be invalid and to be created by the insert,
// we do a pure insert, no upsert allowed.
#(#values)*
let pk = conn.insert_returning_pk(Self::TABLE, &[#insert_cols], &pkcol, &values)?;
#(#post_insert)*
} else {
// Do an upsert
#(#values)*
conn.insert_or_replace(Self::TABLE, &[#insert_cols], &pkcol, &values)?;
self.state.saved = true
}
#many_save
Ok(())
Expand Down
22 changes: 14 additions & 8 deletions butane_core/src/db/pg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,14 +715,20 @@ pub fn sql_insert_or_replace_with_placeholders(
n + 1
});
write!(w, ")").unwrap();
write!(w, " ON CONFLICT ({}) DO UPDATE SET (", pkcol.name()).unwrap();
helper::list_columns(columns, w);
write!(w, ") = (").unwrap();
columns.iter().fold("", |sep, c| {
write!(w, "{}excluded.{}", sep, c.name()).unwrap();
", "
});
write!(w, ")").unwrap();
write!(w, " ON CONFLICT ({}) DO ", pkcol.name()).unwrap();
if columns.len() > 1 {
write!(w, "UPDATE SET (").unwrap();
helper::list_columns(columns, w);
write!(w, ") = (").unwrap();
columns.iter().fold("", |sep, c| {
write!(w, "{}excluded.{}", sep, c.name()).unwrap();
", "
});
write!(w, ")").unwrap();
} else {
// If the pk is the only column and it already exists, then there's nothing to update.
write!(w, "NOTHING").unwrap();
}
}

fn pgtype_for_val(val: &SqlVal) -> postgres::types::Type {
Expand Down
28 changes: 24 additions & 4 deletions butane_core/src/db/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,11 @@ impl ConnectionMethods for rusqlite::Connection {
&self,
table: &str,
columns: &[Column],
_pkcol: &Column,
pkcol: &Column,
values: &[SqlValRef],
) -> Result<()> {
let mut sql = String::new();
sql_insert_or_update(table, columns, &mut sql);
sql_insert_or_update(table, columns, pkcol, &mut sql);
self.execute(&sql, rusqlite::params_from_iter(values))?;
Ok(())
}
Expand Down Expand Up @@ -653,8 +653,8 @@ fn change_column(
result
}

pub fn sql_insert_or_update(table: &str, columns: &[Column], w: &mut impl Write) {
write!(w, "INSERT OR REPLACE ").unwrap();
pub fn sql_insert_or_update(table: &str, columns: &[Column], pkcol: &Column, w: &mut impl Write) {
write!(w, "INSERT ").unwrap();
write!(w, "INTO {} (", helper::quote_reserved_word(table)).unwrap();
helper::list_columns(columns, w);
write!(w, ") VALUES (").unwrap();
Expand All @@ -663,6 +663,26 @@ pub fn sql_insert_or_update(table: &str, columns: &[Column], w: &mut impl Write)
", "
});
write!(w, ")").unwrap();
write!(w, " ON CONFLICT ({}) DO ", pkcol.name()).unwrap();
if columns.len() > 1 {
write!(w, "UPDATE SET (").unwrap();
helper::list_columns(columns, w);
write!(w, ") = (").unwrap();
columns.iter().fold("", |sep, c| {
write!(
w,
"{}excluded.{}",
sep,
helper::quote_reserved_word(c.name())
)
.unwrap();
", "
});
write!(w, ")").unwrap();
} else {
// If the pk is the only column and it already exists, then there's nothing to update.
write!(w, "NOTHING").unwrap();
}
}

#[derive(Debug)]
Expand Down

0 comments on commit d15fe87

Please sign in to comment.