pinakes-core: wrap save_markdown_links in transactions for atomicity

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I44eaeda5bc1d6894137ee9a3c902cdac6a6a6964
This commit is contained in:
raf 2026-02-09 14:26:02 +03:00
commit 4ed61bc62e
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 43 additions and 27 deletions

View file

@ -6047,7 +6047,7 @@ impl StorageBackend for PostgresBackend {
media_id: MediaId, media_id: MediaId,
links: &[crate::model::MarkdownLink], links: &[crate::model::MarkdownLink],
) -> Result<()> { ) -> Result<()> {
let client = self let mut client = self
.pool .pool
.get() .get()
.await .await
@ -6055,9 +6055,14 @@ impl StorageBackend for PostgresBackend {
let media_id_str = media_id.0.to_string(); let media_id_str = media_id.0.to_string();
// 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 // Delete existing links for this source
client tx.execute(
.execute(
"DELETE FROM markdown_links WHERE source_media_id = $1", "DELETE FROM markdown_links WHERE source_media_id = $1",
&[&media_id_str], &[&media_id_str],
) )
@ -6067,8 +6072,7 @@ impl StorageBackend for PostgresBackend {
// Insert new links // Insert new links
for link in links { for link in links {
let target_media_id = link.target_media_id.map(|id| id.0.to_string()); let target_media_id = link.target_media_id.map(|id| id.0.to_string());
client tx.execute(
.execute(
"INSERT INTO markdown_links ( "INSERT INTO markdown_links (
id, source_media_id, target_path, target_media_id, id, source_media_id, target_path, target_media_id,
link_type, link_text, line_number, context, created_at link_type, link_text, line_number, context, created_at
@ -6089,6 +6093,11 @@ impl StorageBackend for PostgresBackend {
.map_err(|e| PinakesError::Database(e.to_string()))?; .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(()) Ok(())
} }

View file

@ -6400,16 +6400,19 @@ impl StorageBackend for SqliteBackend {
let links: Vec<_> = links.to_vec(); let links: Vec<_> = links.to_vec();
tokio::task::spawn_blocking(move || { 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 // Delete existing links for this source
conn.execute( tx.execute(
"DELETE FROM markdown_links WHERE source_media_id = ?1", "DELETE FROM markdown_links WHERE source_media_id = ?1",
[&media_id_str], [&media_id_str],
)?; )?;
// Insert new links // Insert new links
let mut stmt = conn.prepare( let mut stmt = tx.prepare(
"INSERT INTO markdown_links ( "INSERT INTO markdown_links (
id, source_media_id, target_path, target_media_id, id, source_media_id, target_path, target_media_id,
link_type, link_text, line_number, context, created_at 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>(()) Ok::<_, rusqlite::Error>(())
}) })
.await .await