Skip to main content

max / goingson

5.4 KB · 167 lines History Blame Raw
1 //! Batch loading of contact sub-collections.
2 //!
3 //! One query per sub-collection table for a whole page of contacts, then a
4 //! single assembly pass, so listing N contacts costs five queries rather than
5 //! 4N + 1.
6
7 use goingson_core::{
8 Contact, ContactCustomField, ContactEmail, ContactId, ContactPhone, Result, SocialHandle,
9 };
10 use std::collections::HashMap;
11
12 use rusqlite::{Connection, params_from_iter};
13
14 use crate::utils::{bind_placeholders, parse_uuid, query_all};
15
16 use super::row::{
17 ContactEmailRow, ContactPhoneRow, ContactRow, CustomFieldRow, SocialHandleRow,
18 contact_email_from_row, contact_from_row, contact_phone_from_row, custom_field_from_row,
19 social_handle_from_row,
20 };
21
22 /// Batch-load emails for a set of contact IDs.
23 pub(super) fn load_emails_for_contacts(
24 conn: &Connection,
25 ids: &[String],
26 ) -> Result<HashMap<ContactId, Vec<ContactEmail>>> {
27 if ids.is_empty() {
28 return Ok(HashMap::new());
29 }
30
31 let placeholders = bind_placeholders(ids.len());
32 let sql = format!(
33 "SELECT id, contact_id, address, label, is_primary FROM contact_emails WHERE contact_id IN ({placeholders}) ORDER BY is_primary DESC, rowid ASC"
34 );
35
36 let rows = query_all(
37 conn,
38 &sql,
39 params_from_iter(ids.iter()),
40 ContactEmailRow::from_row,
41 )?;
42 let mut map: HashMap<ContactId, Vec<ContactEmail>> = HashMap::new();
43 for row in rows {
44 let contact_id: ContactId = parse_uuid(&row.contact_id)?.into();
45 let email = contact_email_from_row(row)?;
46 map.entry(contact_id).or_default().push(email);
47 }
48 Ok(map)
49 }
50
51 /// Batch-load phones for a set of contact IDs.
52 pub(super) fn load_phones_for_contacts(
53 conn: &Connection,
54 ids: &[String],
55 ) -> Result<HashMap<ContactId, Vec<ContactPhone>>> {
56 if ids.is_empty() {
57 return Ok(HashMap::new());
58 }
59
60 let placeholders = bind_placeholders(ids.len());
61 let sql = format!(
62 "SELECT id, contact_id, number, label, is_primary FROM contact_phones WHERE contact_id IN ({placeholders}) ORDER BY is_primary DESC, rowid ASC"
63 );
64
65 let rows = query_all(
66 conn,
67 &sql,
68 params_from_iter(ids.iter()),
69 ContactPhoneRow::from_row,
70 )?;
71 let mut map: HashMap<ContactId, Vec<ContactPhone>> = HashMap::new();
72 for row in rows {
73 let contact_id: ContactId = parse_uuid(&row.contact_id)?.into();
74 let phone = contact_phone_from_row(row)?;
75 map.entry(contact_id).or_default().push(phone);
76 }
77 Ok(map)
78 }
79
80 /// Batch-load social handles for a set of contact IDs.
81 pub(super) fn load_social_handles_for_contacts(
82 conn: &Connection,
83 ids: &[String],
84 ) -> Result<HashMap<ContactId, Vec<SocialHandle>>> {
85 if ids.is_empty() {
86 return Ok(HashMap::new());
87 }
88
89 let placeholders = bind_placeholders(ids.len());
90 let sql = format!(
91 "SELECT id, contact_id, platform, handle, url FROM contact_social_handles WHERE contact_id IN ({placeholders}) ORDER BY rowid ASC"
92 );
93
94 let rows = query_all(
95 conn,
96 &sql,
97 params_from_iter(ids.iter()),
98 SocialHandleRow::from_row,
99 )?;
100 let mut map: HashMap<ContactId, Vec<SocialHandle>> = HashMap::new();
101 for row in rows {
102 let contact_id: ContactId = parse_uuid(&row.contact_id)?.into();
103 let handle = social_handle_from_row(row)?;
104 map.entry(contact_id).or_default().push(handle);
105 }
106 Ok(map)
107 }
108
109 /// Batch-load custom fields for a set of contact IDs.
110 pub(super) fn load_custom_fields_for_contacts(
111 conn: &Connection,
112 ids: &[String],
113 ) -> Result<HashMap<ContactId, Vec<ContactCustomField>>> {
114 if ids.is_empty() {
115 return Ok(HashMap::new());
116 }
117
118 let placeholders = bind_placeholders(ids.len());
119 let sql = format!(
120 "SELECT id, contact_id, label, value, url FROM contact_custom_fields WHERE contact_id IN ({placeholders}) ORDER BY rowid ASC"
121 );
122
123 let rows = query_all(
124 conn,
125 &sql,
126 params_from_iter(ids.iter()),
127 CustomFieldRow::from_row,
128 )?;
129 let mut map: HashMap<ContactId, Vec<ContactCustomField>> = HashMap::new();
130 for row in rows {
131 let contact_id: ContactId = parse_uuid(&row.contact_id)?.into();
132 let field = custom_field_from_row(row)?;
133 map.entry(contact_id).or_default().push(field);
134 }
135 Ok(map)
136 }
137
138 /// Hydrate a list of contact rows with their sub-collections.
139 pub(super) fn hydrate_contacts(conn: &Connection, rows: Vec<ContactRow>) -> Result<Vec<Contact>> {
140 if rows.is_empty() {
141 return Ok(vec![]);
142 }
143
144 let ids: Vec<String> = rows.iter().map(|r| r.id.clone()).collect();
145 let mut emails_map = load_emails_for_contacts(conn, &ids)?;
146 let mut phones_map = load_phones_for_contacts(conn, &ids)?;
147 let mut social_map = load_social_handles_for_contacts(conn, &ids)?;
148 let mut custom_map = load_custom_fields_for_contacts(conn, &ids)?;
149
150 let mut contacts = Vec::with_capacity(rows.len());
151 for row in rows {
152 let id: ContactId = parse_uuid(&row.id)?.into();
153 let emails = emails_map.remove(&id).unwrap_or_default();
154 let phones = phones_map.remove(&id).unwrap_or_default();
155 let social_handles = social_map.remove(&id).unwrap_or_default();
156 let custom_fields = custom_map.remove(&id).unwrap_or_default();
157 contacts.push(contact_from_row(
158 row,
159 emails,
160 phones,
161 social_handles,
162 custom_fields,
163 )?);
164 }
165 Ok(contacts)
166 }
167