refactor: optimize bulk delete and restore operations for ciphers

This commit is contained in:
qaz741wsd856 2025-12-09 14:48:33 +00:00
parent f5db9e400d
commit eb6a1d8a63

View file

@ -367,23 +367,24 @@ pub async fn soft_delete_ciphers_bulk(
) -> Result<Json<()>, AppError> { ) -> Result<Json<()>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); 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;
let mut statements: Vec<D1PreparedStatement> = Vec::with_capacity(payload.ids.len());
for id in payload.ids { if ids.is_empty() {
let stmt = query!( return Ok(Json(()));
&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);
} }
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?; db::touch_user_updated_at(&db, &claims.sub).await?;
@ -431,9 +432,14 @@ pub async fn hard_delete_ciphers_bulk(
Json(payload): Json<CipherIdsData>, Json(payload): Json<CipherIdsData>,
) -> Result<Json<()>, AppError> { ) -> Result<Json<()>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let batch_size = get_batch_size(&env);
let ids = payload.ids; 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()) { if attachments::attachments_enabled(env.as_ref()) {
let bucket = attachments::require_bucket(env.as_ref())?; let bucket = attachments::require_bucket(env.as_ref())?;
let keys = let keys =
@ -441,21 +447,15 @@ pub async fn hard_delete_ciphers_bulk(
attachments::delete_r2_objects(&bucket, &keys).await?; attachments::delete_r2_objects(&bucket, &keys).await?;
} }
let mut statements: Vec<D1PreparedStatement> = Vec::with_capacity(ids.len()); query!(
&db,
for id in ids { "DELETE FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))",
let stmt = query!( claims.sub,
&db, ids_json
"DELETE FROM ciphers WHERE id = ?1 AND user_id = ?2", )
id, .map_err(|_| AppError::Database)?
claims.sub .run()
) .await?;
.map_err(|_| AppError::Database)?;
statements.push(stmt);
}
db::execute_in_batches(&db, statements, batch_size).await?;
db::touch_user_updated_at(&db, &claims.sub).await?; db::touch_user_updated_at(&db, &claims.sub).await?;
@ -523,7 +523,6 @@ pub async fn restore_ciphers_bulk(
) -> Result<Json<BulkRestoreResponse>, AppError> { ) -> Result<Json<BulkRestoreResponse>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let now = Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(); 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; let ids = payload.ids;
if ids.is_empty() { if ids.is_empty() {
@ -534,25 +533,21 @@ pub async fn restore_ciphers_bulk(
})); }));
} }
// Batch UPDATE operations // Precompute JSON array for ids, used by both UPDATE and SELECT
let mut update_statements: Vec<D1PreparedStatement> = 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
let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; 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<Cipher> = db let mut restored_ciphers: Vec<Cipher> = db
.prepare( .prepare(
"SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))", "SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))",