From eb6a1d8a636a066ef57c87f5921aa01c778f258b Mon Sep 17 00:00:00 2001 From: qaz741wsd856 Date: Tue, 9 Dec 2025 14:48:33 +0000 Subject: [PATCH] refactor: optimize bulk delete and restore operations for ciphers --- src/handlers/ciphers.rs | 91 +++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/src/handlers/ciphers.rs b/src/handlers/ciphers.rs index 7a089e8..d8482b9 100644 --- a/src/handlers/ciphers.rs +++ b/src/handlers/ciphers.rs @@ -367,23 +367,24 @@ pub async fn soft_delete_ciphers_bulk( ) -> Result, AppError> { let db = db::get_db(&env)?; let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); - let batch_size = get_batch_size(&env); - let mut statements: Vec = Vec::with_capacity(payload.ids.len()); + let ids = payload.ids; - for id in payload.ids { - let stmt = query!( - &db, - "UPDATE ciphers SET deleted_at = ?1, updated_at = ?1 WHERE id = ?2 AND user_id = ?3", - now, - id, - claims.sub - ) - .map_err(|_| AppError::Database)?; - - statements.push(stmt); + if ids.is_empty() { + return Ok(Json(())); } - db::execute_in_batches(&db, statements, batch_size).await?; + let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; + + query!( + &db, + "UPDATE ciphers SET deleted_at = ?1, updated_at = ?1 WHERE user_id = ?2 AND id IN (SELECT value FROM json_each(?3))", + now, + claims.sub, + ids_json + ) + .map_err(|_| AppError::Database)? + .run() + .await?; db::touch_user_updated_at(&db, &claims.sub).await?; @@ -431,9 +432,14 @@ pub async fn hard_delete_ciphers_bulk( Json(payload): Json, ) -> Result, AppError> { let db = db::get_db(&env)?; - let batch_size = get_batch_size(&env); let ids = payload.ids; + if ids.is_empty() { + return Ok(Json(())); + } + + let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; + if attachments::attachments_enabled(env.as_ref()) { let bucket = attachments::require_bucket(env.as_ref())?; let keys = @@ -441,21 +447,15 @@ pub async fn hard_delete_ciphers_bulk( attachments::delete_r2_objects(&bucket, &keys).await?; } - let mut statements: Vec = Vec::with_capacity(ids.len()); - - for id in ids { - let stmt = query!( - &db, - "DELETE FROM ciphers WHERE id = ?1 AND user_id = ?2", - id, - claims.sub - ) - .map_err(|_| AppError::Database)?; - - statements.push(stmt); - } - - db::execute_in_batches(&db, statements, batch_size).await?; + query!( + &db, + "DELETE FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))", + claims.sub, + ids_json + ) + .map_err(|_| AppError::Database)? + .run() + .await?; db::touch_user_updated_at(&db, &claims.sub).await?; @@ -523,7 +523,6 @@ pub async fn restore_ciphers_bulk( ) -> Result, AppError> { let db = db::get_db(&env)?; let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); - let batch_size = get_batch_size(&env); let ids = payload.ids; if ids.is_empty() { @@ -534,25 +533,21 @@ pub async fn restore_ciphers_bulk( })); } - // Batch UPDATE operations - let mut update_statements: Vec = Vec::with_capacity(ids.len()); - for id in ids.iter() { - let stmt = query!( - &db, - "UPDATE ciphers SET deleted_at = NULL, updated_at = ?1 WHERE id = ?2 AND user_id = ?3", - now, - id.clone(), - claims.sub - ) - .map_err(|_| AppError::Database)?; - - update_statements.push(stmt); - } - db::execute_in_batches(&db, update_statements, batch_size).await?; - - // Batch SELECT using json_each() - avoid N+1 query problem + // Precompute JSON array for ids, used by both UPDATE and SELECT let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; + // Single bulk UPDATE using json_each() + query!( + &db, + "UPDATE ciphers SET deleted_at = NULL, updated_at = ?1 WHERE user_id = ?2 AND id IN (SELECT value FROM json_each(?3))", + now, + claims.sub, + ids_json.clone() + ) + .map_err(|_| AppError::Database)? + .run() + .await?; + let mut restored_ciphers: Vec = db .prepare( "SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))",