diff --git a/crates/pinakes-core/src/storage/postgres.rs b/crates/pinakes-core/src/storage/postgres.rs index ca7b6ba..ed5633f 100644 --- a/crates/pinakes-core/src/storage/postgres.rs +++ b/crates/pinakes-core/src/storage/postgres.rs @@ -6047,7 +6047,7 @@ impl StorageBackend for PostgresBackend { media_id: MediaId, links: &[crate::model::MarkdownLink], ) -> Result<()> { - let client = self + let mut client = self .pool .get() .await @@ -6055,40 +6055,49 @@ impl StorageBackend for PostgresBackend { let media_id_str = media_id.0.to_string(); - // Delete existing links for this source - client - .execute( - "DELETE FROM markdown_links WHERE source_media_id = $1", - &[&media_id_str], - ) + // Wrap DELETE + INSERT in transaction to ensure atomicity + let tx = client + .transaction() .await .map_err(|e| PinakesError::Database(e.to_string()))?; + // Delete existing links for this source + tx.execute( + "DELETE FROM markdown_links WHERE source_media_id = $1", + &[&media_id_str], + ) + .await + .map_err(|e| PinakesError::Database(e.to_string()))?; + // Insert new links for link in links { let target_media_id = link.target_media_id.map(|id| id.0.to_string()); - client - .execute( - "INSERT INTO markdown_links ( + tx.execute( + "INSERT INTO markdown_links ( id, source_media_id, target_path, target_media_id, link_type, link_text, line_number, context, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", - &[ - &link.id.to_string(), - &media_id_str, - &link.target_path, - &target_media_id, - &link.link_type.to_string(), - &link.link_text, - &link.line_number, - &link.context, - &link.created_at, - ], - ) - .await - .map_err(|e| PinakesError::Database(e.to_string()))?; + &[ + &link.id.to_string(), + &media_id_str, + &link.target_path, + &target_media_id, + &link.link_type.to_string(), + &link.link_text, + &link.line_number, + &link.context, + &link.created_at, + ], + ) + .await + .map_err(|e| PinakesError::Database(e.to_string()))?; } + // Commit transaction - if this fails, all changes are rolled back + tx.commit() + .await + .map_err(|e| PinakesError::Database(e.to_string()))?; + Ok(()) } diff --git a/crates/pinakes-core/src/storage/sqlite.rs b/crates/pinakes-core/src/storage/sqlite.rs index 63c2239..bfd471b 100644 --- a/crates/pinakes-core/src/storage/sqlite.rs +++ b/crates/pinakes-core/src/storage/sqlite.rs @@ -6400,16 +6400,19 @@ impl StorageBackend for SqliteBackend { let links: Vec<_> = links.to_vec(); tokio::task::spawn_blocking(move || { - let conn = conn.lock().unwrap(); + let mut conn = conn.lock().unwrap(); + + // Wrap DELETE + INSERT in transaction to ensure atomicity + let tx = conn.transaction()?; // Delete existing links for this source - conn.execute( + tx.execute( "DELETE FROM markdown_links WHERE source_media_id = ?1", [&media_id_str], )?; // Insert new links - let mut stmt = conn.prepare( + let mut stmt = tx.prepare( "INSERT INTO markdown_links ( id, source_media_id, target_path, target_media_id, link_type, link_text, line_number, context, created_at @@ -6430,6 +6433,10 @@ impl StorageBackend for SqliteBackend { ])?; } + // Commit transaction - if this fails, all changes are rolled back + drop(stmt); + tx.commit()?; + Ok::<_, rusqlite::Error>(()) }) .await