1use std::collections::{BTreeSet, HashMap, HashSet};
6
7use crate::{
8 error::table::TableError,
9 tables::{
10 chat::Chat,
11 diagnostic::{ChatHandleDiagnostic, column_exists},
12 handle::HandleGroups,
13 table::{CHAT_HANDLE_JOIN, CHAT_MESSAGE_JOIN, MESSAGE, Table},
14 },
15 util::union_find::UnionFind,
16};
17use rusqlite::{CachedStatement, Connection, Result, Row};
18
19#[derive(Debug)]
22pub struct ChatToHandle {
23 chat_id: i32,
24 handle_id: i32,
25}
26
27impl Table for ChatToHandle {
28 fn from_row(row: &Row) -> Result<ChatToHandle> {
29 Ok(ChatToHandle {
30 chat_id: row.get("chat_id")?,
31 handle_id: row.get("handle_id")?,
32 })
33 }
34}
35
36impl ChatToHandle {
38 pub fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
40 Ok(db.prepare_cached(&format!("SELECT * FROM {CHAT_HANDLE_JOIN}"))?)
41 }
42
43 pub fn participants_by_chat(
57 db: &Connection,
58 ) -> Result<HashMap<i32, BTreeSet<i32>>, TableError> {
59 let mut cache: HashMap<i32, BTreeSet<i32>> = HashMap::new();
60
61 let mut rows = ChatToHandle::get(db)?;
62 for mapping in ChatToHandle::rows(&mut rows, [])? {
63 let joiner = mapping?;
64 if let Some(handles) = cache.get_mut(&joiner.chat_id) {
65 handles.insert(joiner.handle_id);
66 } else {
67 let mut data_to_cache = BTreeSet::new();
68 data_to_cache.insert(joiner.handle_id);
69 cache.insert(joiner.chat_id, data_to_cache);
70 }
71 }
72
73 Ok(cache)
74 }
75}
76
77#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
82pub enum Participant {
83 Group(i32),
85 Handle(i32),
87}
88
89fn named(chats: &HashMap<i32, Chat>, chat_id: i32) -> bool {
91 chats.get(&chat_id).and_then(Chat::display_name).is_some()
92}
93
94impl ChatToHandle {
96 pub fn run_diagnostic(db: &Connection) -> Result<ChatHandleDiagnostic, TableError> {
112 let mut statement_message_chats =
114 db.prepare(&format!("SELECT DISTINCT chat_id from {CHAT_MESSAGE_JOIN}"))?;
115 let statement_message_chat_rows =
116 statement_message_chats.query_map([], |row: &Row| -> Result<i32> { row.get(0) })?;
117 let mut unique_chats_from_messages: HashSet<i32> = HashSet::new();
118 statement_message_chat_rows.into_iter().for_each(|row| {
119 if let Ok(row) = row {
120 unique_chats_from_messages.insert(row);
121 }
122 });
123
124 let mut statement_handle_chats =
126 db.prepare(&format!("SELECT DISTINCT chat_id from {CHAT_HANDLE_JOIN}"))?;
127 let statement_handle_chat_rows =
128 statement_handle_chats.query_map([], |row: &Row| -> Result<i32> { row.get(0) })?;
129 let mut unique_chats_from_handles: HashSet<i32> = HashSet::new();
130 statement_handle_chat_rows.into_iter().for_each(|row| {
131 if let Ok(row) = row {
132 unique_chats_from_handles.insert(row);
133 }
134 });
135
136 let all_chats = Self::participants_by_chat(db)?;
138
139 let chat_handle_lookup = ChatToHandle::get_chat_lookup_map(db)?;
141 let handle_groups = HandleGroups::from_db(db)?.finalize();
142
143 let chats = Chat::by_rowid(db)?;
145 let real_chatrooms =
146 ChatToHandle::dedupe(&all_chats, &chat_handle_lookup, &handle_groups, &chats);
147
148 let conversations: HashSet<&i32> = all_chats
152 .keys()
153 .filter_map(|chat_id| real_chatrooms.get(chat_id))
154 .collect();
155 let total_duplicated = all_chats.len() - conversations.len();
156
157 let chats_with_no_handles = unique_chats_from_messages
159 .difference(&unique_chats_from_handles)
160 .count();
161
162 Ok(ChatHandleDiagnostic {
163 total_chats: all_chats.len(),
164 total_duplicated,
165 chats_with_no_handles,
166 })
167 }
168}
169
170impl ChatToHandle {
171 pub fn get_chat_lookup_map(conn: &Connection) -> Result<HashMap<i32, i32>, TableError> {
188 let mut stmt = conn.prepare(
190 "
191WITH RECURSIVE
192 adj AS (
193 SELECT DISTINCT a.chat AS u, b.chat AS v
194 FROM chat_lookup a
195 JOIN chat_lookup b
196 ON a.identifier = b.identifier
197 ),
198 reach(root, chat) AS (
199 SELECT u AS root, v AS chat FROM adj
200 UNION
201 SELECT r.root, a.v
202 FROM reach r
203 JOIN adj a ON a.u = r.chat
204 ),
205 canon AS (
206 SELECT chat, MAX(root) AS canonical_chat
207 FROM reach
208 GROUP BY chat
209 )
210SELECT chat, canonical_chat
211FROM canon
212ORDER BY chat;
213 ",
214 );
215 let mut chat_lookup_map: HashMap<i32, i32> = HashMap::new();
216
217 if let Ok(statement) = stmt.as_mut() {
218 let chat_lookup_rows = statement.query_map([], |row| {
220 let chat: i32 = row.get(0)?;
221 let canonical: i32 = row.get(1)?;
222 Ok((chat, canonical))
223 });
224
225 if let Ok(chat_lookup_rows) = chat_lookup_rows {
227 for row in chat_lookup_rows {
228 let (chat_id, canonical_chat) = row?;
229 chat_lookup_map.insert(chat_id, canonical_chat);
230 }
231 }
232 }
233 Ok(chat_lookup_map)
234 }
235
236 pub fn dedupe(
283 duplicated_data: &HashMap<i32, BTreeSet<i32>>,
284 chat_lookup_map: &HashMap<i32, i32>,
285 handle_groups: &HashMap<i32, i32>,
286 chats: &HashMap<i32, Chat>,
287 ) -> HashMap<i32, i32> {
288 let mut uf = UnionFind::new();
289
290 for chat_id in duplicated_data.keys() {
291 uf.make_set(*chat_id);
292 }
293
294 let mut sorted_lookup: Vec<(&i32, &i32)> = chat_lookup_map.iter().collect();
300 sorted_lookup.sort_by_key(|(chat_id, _)| *chat_id);
301 for (chat_id, canonical) in sorted_lookup {
302 uf.union(*chat_id, *canonical);
303 }
304
305 let mut keyed: Vec<(i32, BTreeSet<Participant>, Option<&str>)> = duplicated_data
308 .iter()
309 .map(|(chat_id, participants)| {
310 let key = participants
311 .iter()
312 .map(|handle_id| match handle_groups.get(handle_id) {
313 Some(group) => Participant::Group(*group),
314 None => Participant::Handle(*handle_id),
315 })
316 .collect();
317 let name = chats.get(chat_id).and_then(Chat::display_name);
318 (*chat_id, key, name)
319 })
320 .collect();
321 keyed.sort_by_key(|(id, _, _)| *id);
322
323 let mut first_named: HashMap<(&BTreeSet<Participant>, &str), i32> = HashMap::new();
326 let mut anchor: HashMap<&BTreeSet<Participant>, i32> = HashMap::new();
327 for (chat_id, key, name) in &keyed {
328 let Some(name) = name else { continue };
329 match first_named.get(&(key, *name)) {
330 Some(first_chat) => uf.union(*chat_id, *first_chat),
331 None => {
332 first_named.insert((key, name), *chat_id);
333 }
334 }
335 anchor.entry(key).or_insert(*chat_id);
336 }
337
338 let mut lookup_members: Vec<i32> = chat_lookup_map
347 .iter()
348 .flat_map(|(chat_id, canonical)| [*chat_id, *canonical])
349 .collect();
350 lookup_members.sort_unstable();
351 lookup_members.dedup();
352 let mut named_roots: HashSet<i32> = keyed
353 .iter()
354 .filter(|(_, _, name)| name.is_some())
355 .map(|(chat_id, _, _)| *chat_id)
356 .chain(
357 lookup_members
358 .iter()
359 .copied()
360 .filter(|chat_id| named(chats, *chat_id)),
361 )
362 .map(|chat_id| uf.find(chat_id))
363 .collect();
364 for (chat_id, key, name) in &keyed {
365 if name.is_some() {
366 continue;
367 }
368 let row_named = named_roots.contains(&uf.find(*chat_id));
369 match anchor.get(key) {
370 Some(first_chat) => {
371 let anchor_named = named_roots.contains(&uf.find(*first_chat));
372 if row_named && anchor_named {
373 continue;
374 }
375 uf.union(*chat_id, *first_chat);
376 if row_named || anchor_named {
377 named_roots.insert(uf.find(*chat_id));
378 }
379 }
380 None => {
381 anchor.insert(key, *chat_id);
382 }
383 }
384 }
385
386 let lookup_only = lookup_members
390 .iter()
391 .copied()
392 .filter(|member| !duplicated_data.contains_key(member));
393 uf.label(keyed.iter().map(|(id, _, _)| *id).chain(lookup_only))
394 }
395}
396
397impl ChatToHandle {
399 pub fn get_last_activity(db: &Connection) -> Result<HashMap<i32, i64>, TableError> {
418 let query = if column_exists(db, CHAT_MESSAGE_JOIN, "message_date")? {
419 format!("SELECT chat_id, MAX(message_date) FROM {CHAT_MESSAGE_JOIN} GROUP BY chat_id")
420 } else {
421 format!(
422 "SELECT j.chat_id, MAX(m.date) FROM {CHAT_MESSAGE_JOIN} j JOIN {MESSAGE} m ON m.ROWID = j.message_id GROUP BY j.chat_id"
423 )
424 };
425
426 let mut statement = db.prepare(&query)?;
427 let rows = statement.query_map([], |row| {
428 Ok((row.get::<_, i32>(0)?, row.get::<_, Option<i64>>(1)?))
429 })?;
430
431 let mut last_activity = HashMap::new();
432 for row in rows {
433 let (chat_id, date) = row?;
434 if let Some(date) = date {
435 last_activity.insert(chat_id, date);
436 }
437 }
438 Ok(last_activity)
439 }
440
441 pub fn representatives(
462 deduplicated_chats: &HashMap<i32, i32>,
463 chats: &HashMap<i32, Chat>,
464 last_activity: &HashMap<i32, i64>,
465 participants_by_chat: &HashMap<i32, BTreeSet<i32>>,
466 ) -> HashMap<i32, i32> {
467 let mut members: HashMap<i32, Vec<i32>> = HashMap::new();
468 for (chat_id, dedup_id) in deduplicated_chats {
469 members.entry(*dedup_id).or_default().push(*chat_id);
470 }
471
472 let activity = |chat_id: &i32| {
475 (
476 last_activity.get(chat_id).copied().unwrap_or(i64::MIN),
477 std::cmp::Reverse(*chat_id),
478 )
479 };
480
481 let mut representatives = HashMap::with_capacity(deduplicated_chats.len());
482 for rows in members.into_values() {
483 let most_active = rows
484 .iter()
485 .copied()
486 .max_by_key(activity)
487 .expect("a deduplicated group has at least one row");
488
489 let representative = if named(chats, most_active) {
490 most_active
491 } else {
492 rows.iter()
493 .copied()
494 .filter(|chat_id| named(chats, *chat_id))
495 .min()
496 .or_else(|| {
497 rows.iter()
498 .copied()
499 .filter(|chat_id| participants_by_chat.contains_key(chat_id))
500 .max_by_key(activity)
501 })
502 .unwrap_or(most_active)
503 };
504
505 for chat_id in rows {
506 representatives.insert(chat_id, representative);
507 }
508 }
509
510 representatives
511 }
512}
513
514#[cfg(test)]
516mod tests {
517 use crate::tables::chat_handle::ChatToHandle;
518 use std::collections::{BTreeSet, HashMap, HashSet};
519
520 #[test]
521 fn can_dedupe() {
522 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
523 input.insert(1, BTreeSet::from([1])); input.insert(2, BTreeSet::from([1])); input.insert(3, BTreeSet::from([1])); input.insert(4, BTreeSet::from([2])); input.insert(5, BTreeSet::from([2])); input.insert(6, BTreeSet::from([3])); let output =
531 ChatToHandle::dedupe(&input, &HashMap::new(), &HashMap::new(), &HashMap::new());
532 let expected: HashMap<i32, i32> =
533 HashMap::from([(1, 0), (2, 0), (3, 0), (4, 1), (5, 1), (6, 2)]);
534 assert_eq!(output, expected);
535 }
536
537 #[test]
538 fn can_dedupe_multi() {
539 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
540 input.insert(1, BTreeSet::from([1, 2])); input.insert(2, BTreeSet::from([1])); input.insert(3, BTreeSet::from([1])); input.insert(4, BTreeSet::from([2, 1])); input.insert(5, BTreeSet::from([2, 3])); input.insert(6, BTreeSet::from([3])); let output =
548 ChatToHandle::dedupe(&input, &HashMap::new(), &HashMap::new(), &HashMap::new());
549 let expected: HashMap<i32, i32> =
550 HashMap::from([(1, 0), (2, 1), (3, 1), (4, 0), (5, 2), (6, 3)]);
551 assert_eq!(output, expected);
552 }
553
554 #[test]
555 fn can_dedupe_with_chat_lookup_map() {
556 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
557 input.insert(0, BTreeSet::from([1])); input.insert(1, BTreeSet::from([1])); input.insert(2, BTreeSet::from([3])); input.insert(4, BTreeSet::from([2])); input.insert(5, BTreeSet::from([1])); let chat_lookup_map = HashMap::from([(2, 5), (4, 0)]);
564
565 let output =
566 ChatToHandle::dedupe(&input, &chat_lookup_map, &HashMap::new(), &HashMap::new());
567
568 assert_eq!(output.get(&0), output.get(&1));
571 assert_eq!(output.get(&0), output.get(&4));
572 assert_eq!(output.get(&0), output.get(&5));
573 assert_eq!(output.get(&2), output.get(&5));
574 }
575
576 #[test]
577 fn can_dedupe_with_lookup_map_overriding_participants() {
578 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
579 input.insert(0, BTreeSet::from([1, 2])); input.insert(1, BTreeSet::from([1, 2])); input.insert(2, BTreeSet::from([3, 4])); input.insert(3, BTreeSet::from([1, 2])); let chat_lookup_map = HashMap::from([(1, 0), (2, 0)]);
585
586 let output =
587 ChatToHandle::dedupe(&input, &chat_lookup_map, &HashMap::new(), &HashMap::new());
588
589 assert_eq!(output.get(&0), output.get(&1));
591 assert_eq!(output.get(&0), output.get(&2));
592 assert_eq!(output.get(&3), output.get(&0));
594 }
595
596 #[test]
597 fn can_dedupe_mixed_lookup_and_participants() {
598 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
599 input.insert(0, BTreeSet::from([1])); input.insert(1, BTreeSet::from([1])); input.insert(2, BTreeSet::from([3])); input.insert(3, BTreeSet::from([2])); input.insert(4, BTreeSet::from([3])); let chat_lookup_map = HashMap::from([(1, 0), (3, 0)]);
606
607 let output =
608 ChatToHandle::dedupe(&input, &chat_lookup_map, &HashMap::new(), &HashMap::new());
609
610 assert_eq!(output.get(&0), output.get(&1));
612 assert_eq!(output.get(&0), output.get(&3));
613 assert_ne!(output.get(&2), output.get(&1));
615 assert_eq!(output.get(&4), output.get(&2));
617 assert_ne!(output.get(&3), output.get(&4));
619 }
620
621 #[test]
622 fn lookup_merge_is_order_independent() {
623 let cases = [
627 (
628 "canonical is the lower id",
629 HashMap::from([(0, BTreeSet::from([1, 2])), (5, BTreeSet::from([3, 4]))]),
630 HashMap::from([(0, 0), (5, 0)]),
631 HashMap::from([(0, 0), (5, 0)]),
632 ),
633 (
634 "canonical is the higher id",
635 HashMap::from([(0, BTreeSet::from([1, 2])), (5, BTreeSet::from([3, 4]))]),
636 HashMap::from([(0, 5), (5, 5)]),
637 HashMap::from([(0, 0), (5, 0)]),
638 ),
639 (
640 "iMessage, SMS, and RCS splits share one canonical",
641 HashMap::from([
642 (100, BTreeSet::from([10])),
643 (200, BTreeSet::from([20])),
644 (300, BTreeSet::from([30])),
645 ]),
646 HashMap::from([(100, 300), (200, 300), (300, 300)]),
647 HashMap::from([(100, 0), (200, 0), (300, 0)]),
648 ),
649 ];
650 for (label, input, chat_lookup_map, expected) in cases {
651 let output =
652 ChatToHandle::dedupe(&input, &chat_lookup_map, &HashMap::new(), &HashMap::new());
653 assert_eq!(output, expected, "{label}");
654 }
655 }
656
657 #[test]
658 fn lookup_merges_through_missing_canonical() {
659 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([2]))]);
662
663 let chat_lookup_map = HashMap::from([(10, 99), (20, 99)]);
664
665 let output =
666 ChatToHandle::dedupe(&input, &chat_lookup_map, &HashMap::new(), &HashMap::new());
667
668 assert_eq!(
670 output.get(&10),
671 output.get(&20),
672 "Chats sharing a canonical absent from duplicated_data should still merge"
673 );
674 assert_eq!(output.len(), 3);
677 assert_eq!(output.get(&99), output.get(&10));
678 }
679
680 #[test]
681 fn lookup_component_without_handle_rows_gets_its_own_id() {
682 let input = HashMap::from([(10, BTreeSet::from([1]))]);
683
684 let chat_lookup_map = HashMap::from([(10, 20), (30, 40)]);
685
686 let output =
687 ChatToHandle::dedupe(&input, &chat_lookup_map, &HashMap::new(), &HashMap::new());
688
689 assert_eq!(output.len(), 4);
690 assert_eq!(output.get(&10), Some(&0));
691 assert_eq!(output.get(&20), Some(&0));
692 assert_eq!(output.get(&30), Some(&1));
693 assert_eq!(output.get(&40), Some(&1));
694 }
695
696 #[test]
697 fn chats_differing_only_by_grouped_handles_merge() {
698 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
701 input.insert(10, BTreeSet::from([1, 3]));
702 input.insert(20, BTreeSet::from([2, 3]));
703 input.insert(30, BTreeSet::from([1, 2, 3]));
704
705 let groups: HashMap<i32, i32> = HashMap::from([(1, 0), (2, 0), (3, 1)]);
706
707 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &groups, &HashMap::new());
708
709 assert_eq!(output.get(&10), output.get(&20));
710 assert_eq!(
711 output.get(&10),
712 output.get(&30),
713 "a chat listing both of a person's handles is the same participant set"
714 );
715 }
716
717 #[test]
718 fn chats_differing_by_group_stay_separate() {
719 let input = HashMap::from([(10, BTreeSet::from([1, 3])), (20, BTreeSet::from([2, 3]))]);
720
721 let groups: HashMap<i32, i32> = HashMap::from([(1, 0), (2, 5), (3, 1)]);
722
723 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &groups, &HashMap::new());
724
725 assert_ne!(output.get(&10), output.get(&20));
726 }
727
728 #[test]
729 fn ungrouped_handle_does_not_collide_with_group_id() {
730 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([7]))]);
734
735 let groups: HashMap<i32, i32> = HashMap::from([(1, 7)]);
736
737 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &groups, &HashMap::new());
738
739 assert_ne!(output.get(&10), output.get(&20));
740 }
741
742 #[test]
743 fn grouped_merge_is_transitive_with_lookup() {
744 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
747 input.insert(10, BTreeSet::from([1]));
748 input.insert(20, BTreeSet::from([2]));
749 input.insert(30, BTreeSet::from([9]));
750
751 let groups: HashMap<i32, i32> = HashMap::from([(1, 0), (2, 0), (9, 4)]);
752 let lookup: HashMap<i32, i32> = HashMap::from([(30, 20), (20, 20)]);
753
754 let output = ChatToHandle::dedupe(&input, &lookup, &groups, &HashMap::new());
755
756 let unique: HashSet<i32> = output.values().copied().collect();
757 assert_eq!(unique.len(), 1);
758 }
759
760 fn chat(rowid: i32, name: Option<&str>) -> crate::tables::chat::Chat {
762 crate::tables::chat::Chat {
763 rowid,
764 chat_identifier: String::new(),
765 service_name: None,
766 display_name: name.map(str::to_string),
767 is_filtered: None,
768 is_blackholed: None,
769 is_pending_review: None,
770 }
771 }
772
773 #[test]
774 fn representative_is_the_most_active_named_row() {
775 let dedup: HashMap<i32, i32> = HashMap::from([(10, 0), (20, 0), (30, 1)]);
776 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
777 (10, chat(10, Some("Old Name"))),
778 (20, chat(20, Some("New Name"))),
779 (30, chat(30, None)),
780 ]);
781 let activity: HashMap<i32, i64> = HashMap::from([(10, 100), (20, 200)]);
782
783 let reps = ChatToHandle::representatives(&dedup, &chats, &activity, &HashMap::new());
784
785 assert_eq!(reps[&10], 20);
786 assert_eq!(reps[&20], 20);
787 assert_eq!(reps[&30], 30);
788 }
789
790 #[test]
791 fn unnamed_active_row_defers_to_a_named_sibling() {
792 let dedup: HashMap<i32, i32> = HashMap::from([(10, 0), (20, 0), (25, 0)]);
794 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
795 (10, chat(10, Some("Sardegnas"))),
796 (20, chat(20, Some(""))),
797 (25, chat(25, None)),
798 ]);
799 let activity: HashMap<i32, i64> = HashMap::from([(10, 100), (20, 300), (25, 200)]);
800
801 let reps = ChatToHandle::representatives(&dedup, &chats, &activity, &HashMap::new());
802
803 assert_eq!(reps[&10], 10);
804 assert_eq!(reps[&20], 10);
805 assert_eq!(reps[&25], 10);
806 }
807
808 #[test]
809 fn unnamed_group_uses_its_most_active_row_with_lowest_id_tiebreak() {
810 let dedup: HashMap<i32, i32> = HashMap::from([(10, 0), (20, 0), (30, 0)]);
811 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
812 (10, chat(10, None)),
813 (20, chat(20, None)),
814 (30, chat(30, None)),
815 ]);
816 let activity: HashMap<i32, i64> = HashMap::from([(20, 500), (30, 500)]);
817
818 let reps = ChatToHandle::representatives(&dedup, &chats, &activity, &HashMap::new());
819
820 assert_eq!(reps[&10], 20);
821 assert_eq!(reps[&20], 20);
822 assert_eq!(reps[&30], 20);
823 }
824
825 #[test]
826 fn unnamed_group_representative_has_participant_rows() {
827 let dedup: HashMap<i32, i32> = HashMap::from([(10, 0), (20, 0), (99, 0)]);
828 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
829 (10, chat(10, None)),
830 (20, chat(20, None)),
831 (99, chat(99, None)),
832 ]);
833 let activity: HashMap<i32, i64> = HashMap::from([(10, 100), (20, 200), (99, 300)]);
834 let participants: HashMap<i32, BTreeSet<i32>> =
835 HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([1]))]);
836
837 let reps = ChatToHandle::representatives(&dedup, &chats, &activity, &participants);
838
839 assert_eq!(reps[&10], 20);
840 assert_eq!(reps[&20], 20);
841 assert_eq!(reps[&99], 20);
842 }
843
844 #[test]
845 fn diagnostic_counts_duplicates_over_chats_with_handle_rows() {
846 let db = rusqlite::Connection::open_in_memory().unwrap();
847 db.execute_batch(
848 "CREATE TABLE chat (ROWID INTEGER PRIMARY KEY, chat_identifier TEXT, service_name TEXT, display_name TEXT);
849 CREATE TABLE handle (ROWID INTEGER PRIMARY KEY, id TEXT, person_centric_id TEXT);
850 CREATE TABLE chat_handle_join (chat_id INTEGER, handle_id INTEGER);
851 CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);
852 CREATE TABLE chat_lookup (chat INTEGER, identifier TEXT);
853 INSERT INTO chat VALUES (10, 'a', 'iMessage', NULL), (30, 'b', 'iMessage', NULL), (40, 'c', 'SMS', NULL);
854 INSERT INTO handle VALUES (1, '+15555550100', NULL);
855 INSERT INTO chat_handle_join VALUES (10, 1);
856 INSERT INTO chat_message_join VALUES (10, 1), (30, 2);
857 INSERT INTO chat_lookup VALUES (30, 'x'), (40, 'x');",
858 )
859 .unwrap();
860
861 let diagnostic = ChatToHandle::run_diagnostic(&db).unwrap();
862
863 assert_eq!(diagnostic.total_chats, 1);
864 assert_eq!(diagnostic.total_duplicated, 0);
865 assert_eq!(diagnostic.chats_with_no_handles, 1);
866 }
867
868 #[test]
869 fn last_activity_reads_message_date_when_present() {
870 let db = rusqlite::Connection::open_in_memory().unwrap();
871 db.execute_batch(
872 "CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER, message_date INTEGER DEFAULT 0);
873 INSERT INTO chat_message_join VALUES (1, 1, 10), (1, 2, 30), (2, 3, 20);",
874 )
875 .unwrap();
876
877 let activity = ChatToHandle::get_last_activity(&db).unwrap();
878
879 assert_eq!(activity[&1], 30);
880 assert_eq!(activity[&2], 20);
881 }
882
883 #[test]
884 fn last_activity_falls_back_to_message_table() {
885 let db = rusqlite::Connection::open_in_memory().unwrap();
886 db.execute_batch(
887 "CREATE TABLE chat_message_join (chat_id INTEGER, message_id INTEGER);
888 CREATE TABLE message (ROWID INTEGER PRIMARY KEY, date INTEGER);
889 INSERT INTO message VALUES (1, 10), (2, 30), (3, 20);
890 INSERT INTO chat_message_join VALUES (1, 1), (1, 2), (2, 3);",
891 )
892 .unwrap();
893
894 let activity = ChatToHandle::get_last_activity(&db).unwrap();
895
896 assert_eq!(activity[&1], 30);
897 assert_eq!(activity[&2], 20);
898 }
899
900 #[test]
902 fn same_people_with_different_names_are_different_groups() {
903 let input = HashMap::from([(10, BTreeSet::from([1, 2])), (20, BTreeSet::from([1, 2]))]);
904 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
905 (10, chat(10, Some("Sunday Call"))),
906 (20, chat(20, Some("Da Boys"))),
907 ]);
908
909 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &HashMap::new(), &chats);
910
911 assert_ne!(output.get(&10), output.get(&20));
912 }
913
914 #[test]
915 fn same_people_with_the_same_name_merge() {
916 let input = HashMap::from([(10, BTreeSet::from([1, 2])), (20, BTreeSet::from([1, 2]))]);
917 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
918 (10, chat(10, Some("Family"))),
919 (20, chat(20, Some("Family"))),
920 ]);
921
922 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &HashMap::new(), &chats);
923
924 assert_eq!(output.get(&10), output.get(&20));
925 }
926
927 #[test]
928 fn unnamed_row_joins_the_first_named_row_with_its_people() {
929 let input = HashMap::from([10, 20, 30, 40].map(|id| (id, BTreeSet::from([1, 2]))));
930 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
931 (10, chat(10, Some("Sunday Call"))),
932 (20, chat(20, Some("Da Boys"))),
933 (30, chat(30, None)),
934 (40, chat(40, Some(""))),
935 ]);
936
937 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &HashMap::new(), &chats);
938
939 assert_eq!(output.get(&30), output.get(&10));
940 assert_eq!(output.get(&40), output.get(&10));
941 assert_ne!(output.get(&30), output.get(&20));
942 }
943
944 #[test]
945 fn unnamed_rows_merge_with_each_other_when_nothing_is_named() {
946 let input = HashMap::from([(10, BTreeSet::from([1, 2])), (20, BTreeSet::from([1, 2]))]);
947 let chats: HashMap<i32, crate::tables::chat::Chat> =
948 HashMap::from([(10, chat(10, None)), (20, chat(20, None))]);
949
950 let output = ChatToHandle::dedupe(&input, &HashMap::new(), &HashMap::new(), &chats);
951
952 assert_eq!(output.get(&10), output.get(&20));
953 }
954
955 #[test]
956 fn unnamed_row_keeps_its_chat_lookup_conversation() {
957 let input = HashMap::from([10, 20, 30].map(|id| (id, BTreeSet::from([1, 2]))));
958 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
959 (10, chat(10, Some("Alpha"))),
960 (20, chat(20, Some("Beta"))),
961 (30, chat(30, None)),
962 ]);
963 let lookup: HashMap<i32, i32> = HashMap::from([(30, 20)]);
964
965 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
966
967 assert_eq!(output.get(&30), output.get(&20));
968 assert_ne!(output.get(&10), output.get(&20));
969 }
970
971 #[test]
972 fn unnamed_row_linked_to_an_unnamed_row_still_joins_a_named_row() {
973 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
974 input.insert(10, BTreeSet::from([1, 2]));
975 input.insert(30, BTreeSet::from([1, 2]));
976 input.insert(40, BTreeSet::from([1, 2, 3]));
977 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
978 (10, chat(10, Some("Alpha"))),
979 (30, chat(30, None)),
980 (40, chat(40, None)),
981 ]);
982 let lookup: HashMap<i32, i32> = HashMap::from([(40, 30)]);
983
984 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
985
986 assert_eq!(output.get(&30), output.get(&10));
987 assert_eq!(output.get(&40), output.get(&10));
988 }
989
990 #[test]
991 fn unnamed_anchor_does_not_block_later_unnamed_merges() {
992 let mut input: HashMap<i32, BTreeSet<i32>> = HashMap::new();
993 input.insert(10, BTreeSet::from([1]));
994 input.insert(20, BTreeSet::from([1]));
995 input.insert(30, BTreeSet::from([2]));
996 input.insert(40, BTreeSet::from([2]));
997 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
998 (10, chat(10, None)),
999 (20, chat(20, None)),
1000 (30, chat(30, None)),
1001 (40, chat(40, None)),
1002 ]);
1003 let lookup: HashMap<i32, i32> = HashMap::from([(20, 30)]);
1004
1005 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1006
1007 assert_eq!(output.get(&10), output.get(&20));
1008 assert_eq!(output.get(&20), output.get(&30));
1009 assert_eq!(output.get(&30), output.get(&40));
1010 }
1011
1012 #[test]
1013 fn named_lookup_canonical_without_handle_rows_still_counts_as_named() {
1014 let input = HashMap::from([(10, BTreeSet::from([1, 2])), (20, BTreeSet::from([1, 2]))]);
1015 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
1016 (10, chat(10, Some("Alpha"))),
1017 (20, chat(20, None)),
1018 (99, chat(99, Some("Beta"))),
1019 ]);
1020 let lookup: HashMap<i32, i32> = HashMap::from([(20, 99)]);
1021
1022 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1023
1024 assert_ne!(output.get(&10), output.get(&20));
1025 }
1026
1027 #[test]
1028 fn lookup_members_without_handle_rows_join_the_conversation() {
1029 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([1]))]);
1030 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
1031 (10, chat(10, Some("Beta"))),
1032 (20, chat(20, None)),
1033 (88, chat(88, Some("Alpha"))),
1034 (99, chat(99, None)),
1035 ]);
1036 let lookup: HashMap<i32, i32> = HashMap::from([(20, 99), (88, 99), (99, 99)]);
1037
1038 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1039
1040 assert_ne!(output.get(&10), output.get(&20));
1041 assert_eq!(output.get(&88), output.get(&20));
1044 assert_eq!(output.get(&99), output.get(&20));
1045 }
1046
1047 #[test]
1048 fn unnamed_rows_join_a_sibling_linked_to_a_named_row() {
1049 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([1]))]);
1050 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
1051 (10, chat(10, None)),
1052 (20, chat(20, None)),
1053 (88, chat(88, Some("Alpha"))),
1054 ]);
1055 let lookup: HashMap<i32, i32> = HashMap::from([(10, 88), (88, 88)]);
1056
1057 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1058
1059 assert_eq!(output.get(&10), output.get(&20));
1060 assert_eq!(output.get(&10), output.get(&88));
1061 }
1062
1063 #[test]
1064 fn linked_row_pulls_in_an_earlier_unnamed_anchor() {
1065 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([1]))]);
1066 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
1067 (10, chat(10, None)),
1068 (20, chat(20, None)),
1069 (88, chat(88, Some("Alpha"))),
1070 ]);
1071 let lookup: HashMap<i32, i32> = HashMap::from([(20, 88), (88, 88)]);
1072
1073 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1074
1075 assert_eq!(output.get(&10), output.get(&20));
1076 assert_eq!(output.get(&10), output.get(&88));
1077 }
1078
1079 #[test]
1080 fn unlinked_row_joins_the_lower_of_two_linked_conversations() {
1081 let input = HashMap::from([10, 20, 30].map(|id| (id, BTreeSet::from([1]))));
1082 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
1083 (10, chat(10, None)),
1084 (20, chat(20, None)),
1085 (30, chat(30, None)),
1086 (88, chat(88, Some("Alpha"))),
1087 (99, chat(99, Some("Beta"))),
1088 ]);
1089 let lookup: HashMap<i32, i32> = HashMap::from([(10, 88), (88, 88), (20, 99), (99, 99)]);
1090
1091 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1092
1093 assert_ne!(output.get(&10), output.get(&20));
1094 assert_eq!(output.get(&30), output.get(&10));
1095 assert_eq!(output.get(&88), output.get(&10));
1096 assert_eq!(output.get(&99), output.get(&20));
1097 }
1098
1099 #[test]
1100 fn lookup_only_names_reach_the_representative() {
1101 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([1]))]);
1102 let chats: HashMap<i32, crate::tables::chat::Chat> = HashMap::from([
1103 (10, chat(10, None)),
1104 (20, chat(20, None)),
1105 (88, chat(88, Some("Alpha"))),
1106 (99, chat(99, Some("Beta"))),
1107 ]);
1108 let lookup: HashMap<i32, i32> = HashMap::from([(10, 88), (88, 88), (20, 99), (99, 99)]);
1109
1110 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1111 let representatives =
1112 ChatToHandle::representatives(&output, &chats, &HashMap::new(), &input);
1113
1114 assert_ne!(output.get(&10), output.get(&20));
1115 assert_eq!(representatives.get(&10), Some(&88));
1116 assert_eq!(representatives.get(&20), Some(&99));
1117 }
1118
1119 #[test]
1120 fn chat_lookup_still_joins_differently_named_rows() {
1121 let input = HashMap::from([(10, BTreeSet::from([1])), (20, BTreeSet::from([2]))]);
1123 let chats: HashMap<i32, crate::tables::chat::Chat> =
1124 HashMap::from([(10, chat(10, Some("A"))), (20, chat(20, Some("B")))]);
1125 let lookup: HashMap<i32, i32> = HashMap::from([(10, 20), (20, 20)]);
1126
1127 let output = ChatToHandle::dedupe(&input, &lookup, &HashMap::new(), &chats);
1128
1129 assert_eq!(output.get(&10), output.get(&20));
1130 }
1131}