diff --git a/entity/src/transaction.rs b/entity/src/transaction.rs index cd1f1f9..3ba4dcc 100644 --- a/entity/src/transaction.rs +++ b/entity/src/transaction.rs @@ -16,6 +16,8 @@ pub struct Model { pub notes: Option, pub receipt: Option, pub description: Option, + #[sea_orm(unique)] + pub identity_hash: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/migration/src/lib.rs b/migration/src/lib.rs index c92619b..7811a95 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -1,6 +1,7 @@ pub use sea_orm_migration::prelude::*; pub mod m20230904_141851_create_monzo_tables; +mod m20240529_195030_add_transaction_identity_hash; pub struct Migrator; @@ -11,6 +12,9 @@ impl MigratorTrait for Migrator { } fn migrations() -> Vec> { - vec![Box::new(m20230904_141851_create_monzo_tables::Migration)] + vec![ + Box::new(m20230904_141851_create_monzo_tables::Migration), + Box::new(m20240529_195030_add_transaction_identity_hash::Migration), + ] } } diff --git a/migration/src/m20240529_195030_add_transaction_identity_hash.rs b/migration/src/m20240529_195030_add_transaction_identity_hash.rs new file mode 100644 index 0000000..0577295 --- /dev/null +++ b/migration/src/m20240529_195030_add_transaction_identity_hash.rs @@ -0,0 +1,34 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.alter_table( + TableAlterStatement::new() + .table(Transaction::Table) + .add_column( + ColumnDef::new(Transaction::IdentityHash) + .big_integer() + .unique_key(), + ).to_owned() + ).await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.alter_table( + TableAlterStatement::new() + .table(Transaction::Table) + .drop_column(Transaction::IdentityHash) + .to_owned() + ).await + } +} + +#[derive(DeriveIden)] +enum Transaction { + Table, + IdentityHash, +} diff --git a/src/ingestion/db.rs b/src/ingestion/db.rs index 0edcdec..2695f67 100644 --- a/src/ingestion/db.rs +++ b/src/ingestion/db.rs @@ -82,10 +82,12 @@ async fn update_transactions( insertions: &[Insertion], tx: &DatabaseTransaction, ) -> Result, AppError> { + + let insert = transaction::Entity::insert_many(insertions.iter().map(|i| &i.transaction).cloned()) .on_conflict( - OnConflict::column(transaction::Column::Id) + OnConflict::columns([transaction::Column::Id, transaction::Column::IdentityHash]) .update_columns(transaction::Column::iter()) .to_owned(), ) diff --git a/src/ingestion/ingestion_logic.rs b/src/ingestion/ingestion_logic.rs index de42078..5e2a60d 100644 --- a/src/ingestion/ingestion_logic.rs +++ b/src/ingestion/ingestion_logic.rs @@ -1,3 +1,4 @@ +use std::hash::Hash; use crate::ingestion::db::Insertion; use anyhow::{anyhow, Context}; use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime}; @@ -64,6 +65,17 @@ impl MonzoRow { }.into_active_model()) } + /// Compute a hash of this row, returning the number as an i64 to be used as a unique constraint + /// in the database. + pub fn compute_hash(&self) -> i64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::Hasher; + + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() as i64 + } + pub fn into_insertion(self) -> Result { let expenditures: Vec<_> = match self.category_split { Some(split) if !split.is_empty() => split @@ -90,6 +102,7 @@ impl MonzoRow { receipt: self.receipt, total_amount: self.total_amount, description: self.description, + identity_hash: Some(self.compute_hash()), }.into_active_model(), contained_expenditures: expenditures,