diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index e69abe66..7304e9d7 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -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); diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index 2a8fd0e7..20ab7389 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -71,6 +71,7 @@ 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, @@ -78,10 +79,17 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { 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(()) diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index c9f372a2..a0263202 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -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 { diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index 7640abb2..8be74455 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -198,11 +198,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(()) } @@ -611,8 +611,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(); @@ -621,6 +621,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)]