From 7687766bf7b84f9e87c40950d534c47e39f09bca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 09:52:28 +0000 Subject: [PATCH] perf(dav-collections): indexed UID lookups for single-object operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every CalDAV PUT/GET/DELETE of one .ics loaded the ENTIRE calendar — every row including its ical_data — and filtered with .find() in Rust, so importing N events cost O(N²) rows transferred. CardDAV did the exact same in four places (PROPFIND of one .vcf, PUT existence check, GET, DELETE), with three JSONB deserializations per discarded contact. The indexed repo queries (find_event_by_ical_uid, get_contact_by_uid) existed all along with zero callers. Wire them end to end: new `get_event_by_ical_uid` / `get_contact_by_uid` use-case methods (same access checks as list_events / list_contacts, per the service-layer authz rule) exposed through the storage ports and adapters, and the seven handler sites now resolve one row instead of the whole collection. https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx --- src/application/ports/calendar_ports.rs | 17 ++++++ src/application/ports/carddav_ports.rs | 11 ++++ src/application/services/calendar_service.rs | 23 ++++++++ src/application/services/contact_service.rs | 16 ++++++ .../adapters/calendar_storage_adapter.rs | 20 +++++++ .../adapters/contact_storage_adapter.rs | 18 +++++++ src/interfaces/api/handlers/caldav_handler.rs | 34 +++++------- .../api/handlers/carddav_handler.rs | 53 +++++++------------ 8 files changed, 139 insertions(+), 53 deletions(-) diff --git a/src/application/ports/calendar_ports.rs b/src/application/ports/calendar_ports.rs index 4f8559c6..56799533 100644 --- a/src/application/ports/calendar_ports.rs +++ b/src/application/ports/calendar_ports.rs @@ -87,6 +87,14 @@ pub trait CalendarStoragePort: Send + Sync + 'static { ) -> Result; async fn delete_event(&self, event_id: &str) -> Result<(), DomainError>; async fn get_event(&self, event_id: &str) -> Result; + /// Indexed single-row lookup by iCalendar UID — the CalDAV + /// object-resource paths must use this instead of listing the whole + /// calendar (every row + its `ical_data`) and filtering client-side. + async fn find_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + ) -> Result, DomainError>; async fn list_events_by_calendar( &self, calendar_id: &str, @@ -180,6 +188,15 @@ pub trait CalendarUseCase: Send + Sync + 'static { event_id: &str, user_id: Uuid, ) -> Result; + /// Resolve one event by its iCalendar UID (the identifier CalDAV + /// object resources are addressed by). `Ok(None)` when no event with + /// that UID exists in the calendar. + async fn get_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + user_id: Uuid, + ) -> Result, DomainError>; async fn list_events( &self, calendar_id: &str, diff --git a/src/application/ports/carddav_ports.rs b/src/application/ports/carddav_ports.rs index 94acb1e5..ee49dcec 100644 --- a/src/application/ports/carddav_ports.rs +++ b/src/application/ports/carddav_ports.rs @@ -71,6 +71,17 @@ pub trait ContactUseCase: Send + Sync + 'static { async fn delete_contact(&self, contact_id: &str, user_id: Uuid) -> Result<(), DomainError>; async fn get_contact(&self, contact_id: &str, user_id: Uuid) -> Result; + /// Resolve one contact by its vCard UID (the identifier CardDAV + /// object resources are addressed by) with an indexed single-row + /// lookup — instead of listing the whole address book (every row + /// with its vCard + JSONB columns) and filtering client-side. + /// `Ok(None)` when no contact with that UID exists in the book. + async fn get_contact_by_uid( + &self, + address_book_id: &str, + uid: &str, + user_id: Uuid, + ) -> Result, DomainError>; async fn list_contacts( &self, address_book_id: &str, diff --git a/src/application/services/calendar_service.rs b/src/application/services/calendar_service.rs index 7ce70475..6b51cb8a 100644 --- a/src/application/services/calendar_service.rs +++ b/src/application/services/calendar_service.rs @@ -277,6 +277,29 @@ impl CalendarUseCase for CalendarService { Ok(event) } + async fn get_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + user_id: Uuid, + ) -> Result, DomainError> { + let has_access = self + .calendar_storage + .check_calendar_access(calendar_id, user_id) + .await?; + let calendar = self.calendar_storage.get_calendar(calendar_id).await?; + if !has_access && !calendar.is_public { + return Err(DomainError::new( + ErrorKind::AccessDenied, + "Calendar", + "You don't have permission to view events in this calendar", + )); + } + self.calendar_storage + .find_event_by_ical_uid(calendar_id, ical_uid) + .await + } + async fn list_events( &self, calendar_id: &str, diff --git a/src/application/services/contact_service.rs b/src/application/services/contact_service.rs index f95e9406..9d44b306 100644 --- a/src/application/services/contact_service.rs +++ b/src/application/services/contact_service.rs @@ -780,6 +780,22 @@ impl ContactUseCase for ContactService { Ok(ContactDto::from(contact)) } + async fn get_contact_by_uid( + &self, + address_book_id: &str, + uid: &str, + user_id: Uuid, + ) -> Result, DomainError> { + let id = Uuid::parse_str(address_book_id) + .map_err(|_| DomainError::validation_error("Invalid address book ID format"))?; + + // Check if user has access to the address book + self.check_address_book_access(&id, &user_id).await?; + + let contact = self.contact_repository.get_contact_by_uid(&id, uid).await?; + Ok(contact.map(ContactDto::from)) + } + async fn list_contacts( &self, address_book_id: &str, diff --git a/src/infrastructure/adapters/calendar_storage_adapter.rs b/src/infrastructure/adapters/calendar_storage_adapter.rs index 05b84c08..e37c3ac5 100644 --- a/src/infrastructure/adapters/calendar_storage_adapter.rs +++ b/src/infrastructure/adapters/calendar_storage_adapter.rs @@ -402,6 +402,26 @@ impl CalendarStoragePort for CalendarStorageAdapter { Ok(CalendarEventDto::from(event)) } + async fn find_event_by_ical_uid( + &self, + calendar_id: &str, + ical_uid: &str, + ) -> Result, DomainError> { + let uuid = Uuid::parse_str(calendar_id).map_err(|_| { + DomainError::new( + ErrorKind::InvalidInput, + "Calendar", + "Invalid calendar ID format", + ) + })?; + + let event = self + .event_repository + .find_event_by_ical_uid(&uuid, ical_uid) + .await?; + Ok(event.map(CalendarEventDto::from)) + } + async fn list_events_by_calendar( &self, calendar_id: &str, diff --git a/src/infrastructure/adapters/contact_storage_adapter.rs b/src/infrastructure/adapters/contact_storage_adapter.rs index d629be2c..1f8a06d6 100644 --- a/src/infrastructure/adapters/contact_storage_adapter.rs +++ b/src/infrastructure/adapters/contact_storage_adapter.rs @@ -712,6 +712,24 @@ impl ContactUseCase for ContactStorageAdapter { Ok(ContactDto::from(contact)) } + async fn get_contact_by_uid( + &self, + address_book_id: &str, + uid: &str, + user_id: Uuid, + ) -> Result, DomainError> { + let uuid = Self::parse_uuid(address_book_id, "AddressBook")?; + + // Check read access + self.check_address_book_access(&uuid, user_id).await?; + + let contact = self + .contact_repository + .get_contact_by_uid(&uuid, uid) + .await?; + Ok(contact.map(ContactDto::from)) + } + async fn list_contacts( &self, address_book_id: &str, diff --git a/src/interfaces/api/handlers/caldav_handler.rs b/src/interfaces/api/handlers/caldav_handler.rs index 99179144..98fded62 100644 --- a/src/interfaces/api/handlers/caldav_handler.rs +++ b/src/interfaces/api/handlers/caldav_handler.rs @@ -608,12 +608,13 @@ async fn handle_put( let ical_uid = extract_uid_from_ical(&ical_data); + // Indexed single-row lookup — listing the whole calendar (every row + // with its ical_data) to find one UID made imports O(N²). let existing = if let Some(ref uid) = ical_uid { - let events = calendar_service - .list_events(calendar_id, None, None, user.id) + calendar_service + .get_event_by_ical_uid(calendar_id, uid, user.id) .await - .unwrap_or_default(); - events.into_iter().find(|e| e.ical_uid == *uid) + .unwrap_or_default() } else { None }; @@ -704,21 +705,17 @@ async fn handle_get( .body(Body::from(ical)) .unwrap()) } else { - // GET on individual event + // GET on individual event — indexed lookup by iCalendar UID. let event_file = parts[1]; let ical_uid = event_file.trim_end_matches(".ics"); - let events = calendar_service - .list_events(calendar_id, None, None, user.id) + let event = calendar_service + .get_event_by_ical_uid(calendar_id, ical_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; - - let event = events - .iter() - .find(|e| e.ical_uid == ical_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up event: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?; - let ical = generate_event_ical(event); + let ical = generate_event_ical(&event); Ok(Response::builder() .status(StatusCode::OK) @@ -813,14 +810,11 @@ async fn handle_delete( let event_file = parts[1]; let ical_uid = event_file.trim_end_matches(".ics"); - let events = calendar_service - .list_events(calendar_id, None, None, user.id) + // Indexed lookup by iCalendar UID instead of listing the calendar. + let event = calendar_service + .get_event_by_ical_uid(calendar_id, ical_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list events: {}", e)))?; - - let event = events - .iter() - .find(|e| e.ical_uid == ical_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up event: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Event not found: {}", ical_uid)))?; calendar_service diff --git a/src/interfaces/api/handlers/carddav_handler.rs b/src/interfaces/api/handlers/carddav_handler.rs index 3a6d273e..5b58c1fd 100644 --- a/src/interfaces/api/handlers/carddav_handler.rs +++ b/src/interfaces/api/handlers/carddav_handler.rs @@ -295,19 +295,14 @@ async fn handle_propfind( .body(Body::from(response_body)) .unwrap()) } else { - // Individual contact .vcf + // Individual contact .vcf — indexed lookup by vCard UID. let contact_file = parts[1]; let contact_uid = contact_file.trim_end_matches(".vcf"); - // Look up by UID across all contacts in this address book - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + let contact = contact_svc + .get_contact_by_uid(address_book_id, contact_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; - - let contact = contacts - .iter() - .find(|c| c.uid == contact_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up contact: {}", e)))? .ok_or_else(|| { AppError::not_found(format!("Contact not found: {}", contact_uid)) })?; @@ -322,8 +317,8 @@ async fn handle_propfind( let mut response_body = Vec::new(); CardDavAdapter::generate_contacts_response( &mut response_body, - std::slice::from_ref(contact), - &[(contact.uid.clone(), contact_to_vcard(contact))], + std::slice::from_ref(&contact), + &[(contact.uid.clone(), contact_to_vcard(&contact))], &report, base_href, ) @@ -483,13 +478,13 @@ async fn handle_put( // Extract UID from vCard let vcard_uid = extract_uid_from_vcard(&vcard_data); - // Check if contact already exists + // Check if contact already exists — indexed single-row lookup + // (listing the whole address book made imports O(N²)). let existing = if let Some(ref uid) = vcard_uid { - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + contact_svc + .get_contact_by_uid(address_book_id, uid, user.id) .await - .unwrap_or_default(); - contacts.into_iter().find(|c| c.uid == *uid) + .unwrap_or_default() } else { None }; @@ -579,21 +574,17 @@ async fn handle_get( .body(Body::from(vcf_data)) .unwrap()) } else { - // GET on individual contact + // GET on individual contact — indexed lookup by vCard UID. let contact_file = parts[1]; let contact_uid = contact_file.trim_end_matches(".vcf"); - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + let contact = contact_svc + .get_contact_by_uid(address_book_id, contact_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; - - let contact = contacts - .iter() - .find(|c| c.uid == contact_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up contact: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?; - let vcard = contact_to_vcard(contact); + let vcard = contact_to_vcard(&contact); Ok(Response::builder() .status(StatusCode::OK) @@ -632,18 +623,14 @@ async fn handle_delete( AppError::internal_error(format!("Failed to delete address book: {}", e)) })?; } else { - // Delete contact + // Delete contact — indexed lookup by vCard UID. let contact_file = parts[1]; let contact_uid = contact_file.trim_end_matches(".vcf"); - let contacts = contact_svc - .list_contacts(address_book_id, user.id) + let contact = contact_svc + .get_contact_by_uid(address_book_id, contact_uid, user.id) .await - .map_err(|e| AppError::internal_error(format!("Failed to list contacts: {}", e)))?; - - let contact = contacts - .iter() - .find(|c| c.uid == contact_uid) + .map_err(|e| AppError::internal_error(format!("Failed to look up contact: {}", e)))? .ok_or_else(|| AppError::not_found(format!("Contact not found: {}", contact_uid)))?; contact_svc