//! Batch loading of contact sub-collections. //! //! One query per sub-collection table for a whole page of contacts, then a //! single assembly pass, so listing N contacts costs five queries rather than //! 4N + 1. use goingson_core::{ Contact, ContactCustomField, ContactEmail, ContactId, ContactPhone, Result, SocialHandle, }; use std::collections::HashMap; use rusqlite::{Connection, params_from_iter}; use crate::utils::{bind_placeholders, parse_uuid, query_all}; use super::row::{ ContactEmailRow, ContactPhoneRow, ContactRow, CustomFieldRow, SocialHandleRow, contact_email_from_row, contact_from_row, contact_phone_from_row, custom_field_from_row, social_handle_from_row, }; /// Batch-load emails for a set of contact IDs. pub(super) fn load_emails_for_contacts( conn: &Connection, ids: &[String], ) -> Result>> { if ids.is_empty() { return Ok(HashMap::new()); } let placeholders = bind_placeholders(ids.len()); let sql = format!( "SELECT id, contact_id, address, label, is_primary FROM contact_emails WHERE contact_id IN ({placeholders}) ORDER BY is_primary DESC, rowid ASC" ); let rows = query_all( conn, &sql, params_from_iter(ids.iter()), ContactEmailRow::from_row, )?; let mut map: HashMap> = HashMap::new(); for row in rows { let contact_id: ContactId = parse_uuid(&row.contact_id)?.into(); let email = contact_email_from_row(row)?; map.entry(contact_id).or_default().push(email); } Ok(map) } /// Batch-load phones for a set of contact IDs. pub(super) fn load_phones_for_contacts( conn: &Connection, ids: &[String], ) -> Result>> { if ids.is_empty() { return Ok(HashMap::new()); } let placeholders = bind_placeholders(ids.len()); let sql = format!( "SELECT id, contact_id, number, label, is_primary FROM contact_phones WHERE contact_id IN ({placeholders}) ORDER BY is_primary DESC, rowid ASC" ); let rows = query_all( conn, &sql, params_from_iter(ids.iter()), ContactPhoneRow::from_row, )?; let mut map: HashMap> = HashMap::new(); for row in rows { let contact_id: ContactId = parse_uuid(&row.contact_id)?.into(); let phone = contact_phone_from_row(row)?; map.entry(contact_id).or_default().push(phone); } Ok(map) } /// Batch-load social handles for a set of contact IDs. pub(super) fn load_social_handles_for_contacts( conn: &Connection, ids: &[String], ) -> Result>> { if ids.is_empty() { return Ok(HashMap::new()); } let placeholders = bind_placeholders(ids.len()); let sql = format!( "SELECT id, contact_id, platform, handle, url FROM contact_social_handles WHERE contact_id IN ({placeholders}) ORDER BY rowid ASC" ); let rows = query_all( conn, &sql, params_from_iter(ids.iter()), SocialHandleRow::from_row, )?; let mut map: HashMap> = HashMap::new(); for row in rows { let contact_id: ContactId = parse_uuid(&row.contact_id)?.into(); let handle = social_handle_from_row(row)?; map.entry(contact_id).or_default().push(handle); } Ok(map) } /// Batch-load custom fields for a set of contact IDs. pub(super) fn load_custom_fields_for_contacts( conn: &Connection, ids: &[String], ) -> Result>> { if ids.is_empty() { return Ok(HashMap::new()); } let placeholders = bind_placeholders(ids.len()); let sql = format!( "SELECT id, contact_id, label, value, url FROM contact_custom_fields WHERE contact_id IN ({placeholders}) ORDER BY rowid ASC" ); let rows = query_all( conn, &sql, params_from_iter(ids.iter()), CustomFieldRow::from_row, )?; let mut map: HashMap> = HashMap::new(); for row in rows { let contact_id: ContactId = parse_uuid(&row.contact_id)?.into(); let field = custom_field_from_row(row)?; map.entry(contact_id).or_default().push(field); } Ok(map) } /// Hydrate a list of contact rows with their sub-collections. pub(super) fn hydrate_contacts(conn: &Connection, rows: Vec) -> Result> { if rows.is_empty() { return Ok(vec![]); } let ids: Vec = rows.iter().map(|r| r.id.clone()).collect(); let mut emails_map = load_emails_for_contacts(conn, &ids)?; let mut phones_map = load_phones_for_contacts(conn, &ids)?; let mut social_map = load_social_handles_for_contacts(conn, &ids)?; let mut custom_map = load_custom_fields_for_contacts(conn, &ids)?; let mut contacts = Vec::with_capacity(rows.len()); for row in rows { let id: ContactId = parse_uuid(&row.id)?.into(); let emails = emails_map.remove(&id).unwrap_or_default(); let phones = phones_map.remove(&id).unwrap_or_default(); let social_handles = social_map.remove(&id).unwrap_or_default(); let custom_fields = custom_map.remove(&id).unwrap_or_default(); contacts.push(contact_from_row( row, emails, phones, social_handles, custom_fields, )?); } Ok(contacts) }