1use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use anyhow::{Context, Result};
10use rusqlite::{Connection, OpenFlags, params};
11use serde_json::Value;
12use tracing::info;
13
14use super::columns::DetectedSchema;
15use super::entities::{Attachment, Chat, Handle, Message};
16use super::row_reader;
17use super::transformers::date_to_db;
18use super::types::*;
19
20pub struct MessageRepository {
22 conn: Connection,
23 schema: Arc<DetectedSchema>,
24}
25
26fn json_to_sql(val: &Value) -> Box<dyn rusqlite::types::ToSql> {
28 match val {
29 Value::String(s) => Box::new(s.clone()),
30 Value::Number(n) => {
31 if let Some(i) = n.as_i64() {
32 Box::new(i)
33 } else if let Some(f) = n.as_f64() {
34 Box::new(f)
35 } else {
36 Box::new(n.to_string())
37 }
38 }
39 Value::Bool(b) => Box::new(if *b { 1i64 } else { 0i64 }),
40 Value::Null => Box::new(Option::<String>::None),
41 _ => Box::new(val.to_string()),
42 }
43}
44
45fn expand_named_params(
47 statement: &str,
48 args: &HashMap<String, Value>,
49 bind_values: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
50) -> String {
51 let mut result = String::with_capacity(statement.len());
52 let chars: Vec<char> = statement.chars().collect();
53 let len = chars.len();
54 let mut i = 0;
55
56 while i < len {
57 if chars[i] == ':' {
58 let is_spread =
59 i + 3 < len && chars[i + 1] == '.' && chars[i + 2] == '.' && chars[i + 3] == '.';
60 let name_start = if is_spread { i + 4 } else { i + 1 };
61
62 let mut name_end = name_start;
63 while name_end < len && (chars[name_end].is_alphanumeric() || chars[name_end] == '_') {
64 name_end += 1;
65 }
66
67 if name_end > name_start {
68 let name: String = chars[name_start..name_end].iter().collect();
69 if let Some(val) = args.get(&name) {
70 if is_spread {
71 if let Some(arr) = val.as_array() {
72 for (j, item) in arr.iter().enumerate() {
73 if j > 0 {
74 result.push_str(", ");
75 }
76 result.push('?');
77 bind_values.push(json_to_sql(item));
78 }
79 }
80 } else {
81 result.push('?');
82 bind_values.push(json_to_sql(val));
83 }
84 i = name_end;
85 continue;
86 }
87 }
88 }
89 result.push(chars[i]);
90 i += 1;
91 }
92
93 result
94}
95
96fn apply_where_clauses(
98 sql: &mut String,
99 bind_values: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
100 where_clauses: &[WhereClause],
101) {
102 for clause in where_clauses {
103 let expanded = expand_named_params(&clause.statement, &clause.args, bind_values);
104 sql.push_str(&format!(" AND ({expanded})"));
105 }
106}
107
108impl MessageRepository {
109 pub fn open(db_path: PathBuf) -> Result<Self> {
111 let conn = Connection::open_with_flags(
112 &db_path,
113 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
114 )
115 .with_context(|| format!("Failed to open iMessage DB at {}", db_path.display()))?;
116
117 let schema = Arc::new(DetectedSchema::detect(&conn));
118 info!(
119 "iMessage DB opened: {} message columns detected",
120 schema.message_select_columns().len()
121 );
122
123 Ok(Self { conn, schema })
124 }
125
126 pub fn schema(&self) -> &DetectedSchema {
128 &self.schema
129 }
130
131 pub fn get_message_count(&self, params: &MessageCountParams) -> Result<i64> {
137 let mut sql = String::from("SELECT COUNT(*) FROM message");
138 let mut bind_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
139
140 if let Some(ref guid) = params.chat_guid {
142 sql.push_str(
143 " INNER JOIN chat_message_join ON message.ROWID = chat_message_join.message_id \
144 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id",
145 );
146 sql.push_str(" WHERE chat.guid = ?");
147 bind_values.push(Box::new(guid.clone()));
148 } else {
149 sql.push_str(" WHERE 1=1");
150 }
151
152 if params.is_from_me {
153 sql.push_str(" AND message.is_from_me = 1");
154 }
155
156 if let Some(min) = params.min_row_id {
157 sql.push_str(" AND message.ROWID >= ?");
158 bind_values.push(Box::new(min));
159 }
160
161 if let Some(max) = params.max_row_id {
162 sql.push_str(" AND message.ROWID <= ?");
163 bind_values.push(Box::new(max));
164 }
165
166 if params.after.is_some() || params.before.is_some() {
168 if params.updated {
169 self.append_update_date_sql(
170 &mut sql,
171 &mut bind_values,
172 params.after,
173 params.before,
174 false,
175 );
176 } else {
177 self.append_date_sql(&mut sql, &mut bind_values, params.after, params.before);
178 }
179 }
180
181 apply_where_clauses(&mut sql, &mut bind_values, ¶ms.where_clauses);
183
184 let bind_refs: Vec<&dyn rusqlite::types::ToSql> =
185 bind_values.iter().map(|b| b.as_ref()).collect();
186 let count: i64 = self
187 .conn
188 .query_row(&sql, bind_refs.as_slice(), |row| row.get(0))?;
189 Ok(count)
190 }
191
192 pub fn get_chat_count(&self) -> Result<i64> {
194 let count: i64 = self
195 .conn
196 .query_row("SELECT COUNT(*) FROM chat", [], |row| row.get(0))?;
197 Ok(count)
198 }
199
200 pub fn get_handle_count(&self, address: Option<&str>) -> Result<i64> {
202 if let Some(addr) = address {
203 let stripped = addr.replace('+', "");
204 let count: i64 = self.conn.query_row(
205 "SELECT COUNT(*) FROM handle WHERE handle.id LIKE ?",
206 [format!("%{stripped}")],
207 |row| row.get(0),
208 )?;
209 Ok(count)
210 } else {
211 let count: i64 = self
212 .conn
213 .query_row("SELECT COUNT(*) FROM handle", [], |row| row.get(0))?;
214 Ok(count)
215 }
216 }
217
218 pub fn get_attachment_count(&self) -> Result<i64> {
220 let count: i64 = self
221 .conn
222 .query_row("SELECT COUNT(*) FROM attachment", [], |row| row.get(0))?;
223 Ok(count)
224 }
225
226 pub fn get_message(
232 &self,
233 guid: &str,
234 with_chats: bool,
235 with_attachments: bool,
236 ) -> Result<Option<Message>> {
237 let msg_cols = self.schema.message_select_columns();
238 let msg_select = msg_cols.join(", ");
239
240 let sql = format!(
241 "SELECT {msg_select}, \
242 handle.ROWID AS h_ROWID, handle.id AS h_id, handle.country AS h_country, \
243 handle.service AS h_service, handle.uncanonicalized_id AS h_uncanonicalized_id \
244 FROM message \
245 LEFT JOIN handle ON message.handle_id = handle.ROWID \
246 WHERE message.guid = ?"
247 );
248
249 let mut stmt = self.conn.prepare(&sql)?;
250 let result = stmt.query_row(params![guid], |row| {
251 let mut msg = row_reader::read_message(row, &self.schema);
252 msg.handle = row_reader::read_handle_from_join(row, "h_");
253 Ok(msg)
254 });
255
256 let mut msg = match result {
257 Ok(msg) => msg,
258 Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
259 Err(e) => return Err(e.into()),
260 };
261
262 if with_chats {
265 msg.chats = self.get_chats_for_message(msg.rowid)?;
266 }
267
268 if with_attachments {
270 msg.attachments = self.get_attachments_for_message(msg.rowid)?;
271 }
272
273 Ok(Some(msg))
274 }
275
276 pub fn get_attachment(&self, attachment_guid: &str) -> Result<Option<Attachment>> {
278 let att_cols = self.schema.attachment_select_columns();
279 let att_select = att_cols.join(", ");
280
281 let mut lookup_guids = vec![attachment_guid.to_string()];
283 if attachment_guid.len() > 36 {
284 lookup_guids.push(attachment_guid[attachment_guid.len() - 36..].to_string());
285 }
286
287 for lookup_guid in &lookup_guids {
288 let like_pattern = format!("%{lookup_guid}");
289
290 let sql = format!(
291 "SELECT {att_select} FROM attachment \
292 WHERE attachment.original_guid LIKE ?1 OR attachment.guid LIKE ?1 \
293 LIMIT 1"
294 );
295
296 let mut stmt = self.conn.prepare(&sql)?;
297 let result = stmt.query_row(params![like_pattern], |row| {
298 Ok(row_reader::read_attachment(row, &self.schema))
299 });
300
301 match result {
302 Ok(att) => return Ok(Some(att)),
303 Err(rusqlite::Error::QueryReturnedNoRows) => continue,
304 Err(e) => return Err(e.into()),
305 }
306 }
307
308 Ok(None)
309 }
310
311 pub fn get_imessage_account(&self) -> Result<Option<String>> {
313 let result = self.conn.query_row(
314 "SELECT account_login FROM chat WHERE service_name = 'iMessage' ORDER BY ROWID DESC LIMIT 1",
315 [],
316 |row| row.get::<_, Option<String>>(0),
317 );
318
319 match result {
320 Ok(Some(login)) => Ok(login.split(':').next_back().map(|s| s.to_string())),
321 Ok(None) => Ok(None),
322 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
323 Err(e) => Err(e.into()),
324 }
325 }
326
327 pub fn get_chat_last_message(&self, chat_guid: &str) -> Result<Option<Message>> {
329 let msg_cols = self.schema.message_select_columns();
330 let msg_select = msg_cols.join(", ");
331
332 let sql = format!(
333 "SELECT {msg_select}, \
334 handle.ROWID AS h_ROWID, handle.id AS h_id, handle.country AS h_country, \
335 handle.service AS h_service, handle.uncanonicalized_id AS h_uncanonicalized_id \
336 FROM message \
337 LEFT JOIN handle ON message.handle_id = handle.ROWID \
338 INNER JOIN chat_message_join ON message.ROWID = chat_message_join.message_id \
339 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id \
340 WHERE chat.guid = ? \
341 ORDER BY message.date DESC \
342 LIMIT 1"
343 );
344
345 let mut stmt = self.conn.prepare(&sql)?;
346 let result = stmt.query_row(params![chat_guid], |row| {
347 let mut msg = row_reader::read_message(row, &self.schema);
348 msg.handle = row_reader::read_handle_from_join(row, "h_");
349 Ok(msg)
350 });
351
352 match result {
353 Ok(msg) => Ok(Some(msg)),
354 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
355 Err(e) => Err(e.into()),
356 }
357 }
358
359 pub fn get_messages(&self, params: &MessageQueryParams) -> Result<(Vec<Message>, i64)> {
365 let msg_cols = self.schema.message_select_columns();
366 let msg_select = msg_cols.join(", ");
367
368 let mut sql = format!(
369 "SELECT {msg_select}, \
370 handle.ROWID AS h_ROWID, handle.id AS h_id, handle.country AS h_country, \
371 handle.service AS h_service, handle.uncanonicalized_id AS h_uncanonicalized_id \
372 FROM message \
373 LEFT JOIN handle ON message.handle_id = handle.ROWID"
374 );
375
376 let mut bind_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
377
378 if let Some(ref guid) = params.chat_guid {
380 sql.push_str(
381 " INNER JOIN chat_message_join ON message.ROWID = chat_message_join.message_id \
382 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id",
383 );
384 sql.push_str(" WHERE chat.guid = ?");
385 bind_values.push(Box::new(guid.clone()));
386 } else if params.with_chats {
387 sql.push_str(
388 " INNER JOIN chat_message_join ON message.ROWID = chat_message_join.message_id \
389 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id",
390 );
391 sql.push_str(" WHERE 1=1");
392 } else {
393 sql.push_str(" WHERE 1=1");
394 }
395
396 if params.after.is_some() || params.before.is_some() {
398 self.append_date_sql(&mut sql, &mut bind_values, params.after, params.before);
399 }
400
401 apply_where_clauses(&mut sql, &mut bind_values, ¶ms.where_clauses);
403
404 let order_col = ¶ms.order_by;
406 let sort = params.sort.as_sql();
407 sql.push_str(&format!(" ORDER BY {order_col} {sort}"));
408 sql.push_str(&format!(" LIMIT {} OFFSET {}", params.limit, params.offset));
409
410 let bind_refs: Vec<&dyn rusqlite::types::ToSql> =
411 bind_values.iter().map(|b| b.as_ref()).collect();
412
413 let mut stmt = self.conn.prepare(&sql)?;
414 let rows = stmt.query_map(bind_refs.as_slice(), |row| {
415 let mut msg = row_reader::read_message(row, &self.schema);
416 msg.handle = row_reader::read_handle_from_join(row, "h_");
417 Ok(msg)
418 })?;
419
420 let mut messages: Vec<Message> = vec![];
421 for row_result in rows {
422 messages.push(row_result?);
423 }
424
425 if params.with_attachments {
427 for msg in &mut messages {
428 msg.attachments = self.get_attachments_for_message(msg.rowid)?;
429 }
430 }
431
432 if params.with_chats && params.chat_guid.is_none() {
434 for msg in &mut messages {
435 msg.chats = self.get_chats_for_message(msg.rowid)?;
436 }
437 }
438
439 let count_params = MessageCountParams {
441 chat_guid: params.chat_guid.clone(),
442 after: params.after,
443 before: params.before,
444 where_clauses: params.where_clauses.clone(),
445 ..Default::default()
446 };
447 let total = self.get_message_count(&count_params)?;
448
449 Ok((messages, total))
450 }
451
452 pub fn get_updated_messages(&self, params: &UpdatedMessageQueryParams) -> Result<Vec<Message>> {
454 let msg_cols = self.schema.message_select_columns();
455 let msg_select = msg_cols.join(", ");
456
457 let mut sql = format!(
458 "SELECT {msg_select}, \
459 handle.ROWID AS h_ROWID, handle.id AS h_id, handle.country AS h_country, \
460 handle.service AS h_service, handle.uncanonicalized_id AS h_uncanonicalized_id \
461 FROM message \
462 LEFT JOIN handle ON message.handle_id = handle.ROWID"
463 );
464
465 let mut bind_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
466
467 if let Some(ref guid) = params.chat_guid {
468 sql.push_str(
469 " INNER JOIN chat_message_join ON message.ROWID = chat_message_join.message_id \
470 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id",
471 );
472 sql.push_str(" WHERE chat.guid = ?");
473 bind_values.push(Box::new(guid.clone()));
474 } else if params.with_chats {
475 sql.push_str(
476 " INNER JOIN chat_message_join ON message.ROWID = chat_message_join.message_id \
477 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id",
478 );
479 sql.push_str(" WHERE 1=1");
480 } else {
481 sql.push_str(" WHERE 1=1");
482 }
483
484 if params.after.is_some() || params.before.is_some() {
485 self.append_update_date_sql(
486 &mut sql,
487 &mut bind_values,
488 params.after,
489 params.before,
490 params.include_created,
491 );
492 }
493
494 let sort = params.sort.as_sql();
495 sql.push_str(&format!(" ORDER BY message.date {sort}"));
496 sql.push_str(&format!(" LIMIT {} OFFSET {}", params.limit, params.offset));
497
498 let bind_refs: Vec<&dyn rusqlite::types::ToSql> =
499 bind_values.iter().map(|b| b.as_ref()).collect();
500 let mut stmt = self.conn.prepare(&sql)?;
501 let rows = stmt.query_map(bind_refs.as_slice(), |row| {
502 let mut msg = row_reader::read_message(row, &self.schema);
503 msg.handle = row_reader::read_handle_from_join(row, "h_");
504 Ok(msg)
505 })?;
506
507 let mut messages: Vec<Message> = vec![];
508 for row_result in rows {
509 messages.push(row_result?);
510 }
511
512 if params.with_attachments {
513 for msg in &mut messages {
514 msg.attachments = self.get_attachments_for_message(msg.rowid)?;
515 }
516 }
517
518 Ok(messages)
519 }
520
521 pub fn get_chats(&self, params: &ChatQueryParams) -> Result<(Vec<Chat>, i64)> {
523 let chat_cols = self.schema.chat_select_columns();
524 let chat_select = chat_cols.join(", ");
525
526 let mut sql = format!("SELECT {chat_select} FROM chat WHERE 1=1");
527 let mut bind_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
528
529 if !params.with_archived {
530 sql.push_str(" AND chat.is_archived = 0");
531 }
532
533 if let Some(ref guid) = params.chat_guid {
534 if params.glob_guid {
535 sql.push_str(" AND chat.guid LIKE ?");
536 bind_values.push(Box::new(format!("%{guid}%")));
537 } else {
538 sql.push_str(" AND chat.guid = ?");
539 bind_values.push(Box::new(guid.clone()));
540 }
541 }
542
543 let order_col = ¶ms.order_by;
544 sql.push_str(&format!(" ORDER BY {order_col} DESC"));
545
546 if let Some(limit) = params.limit {
547 sql.push_str(&format!(" LIMIT {limit} OFFSET {}", params.offset));
548 }
549
550 let bind_refs: Vec<&dyn rusqlite::types::ToSql> =
551 bind_values.iter().map(|b| b.as_ref()).collect();
552 let mut stmt = self.conn.prepare(&sql)?;
553 let rows = stmt.query_map(bind_refs.as_slice(), |row| {
554 Ok(row_reader::read_chat(row, &self.schema))
555 })?;
556
557 let mut chats: Vec<Chat> = vec![];
558 for row_result in rows {
559 chats.push(row_result?);
560 }
561
562 if params.with_participants {
564 for chat in &mut chats {
565 chat.participants = self.get_chat_participants(chat.rowid)?;
566 }
567 }
568
569 if params.with_last_message {
571 for chat in &mut chats {
572 if let Some(msg) = self.get_chat_last_message(&chat.guid)? {
573 chat.messages = vec![msg];
574 }
575 }
576 }
577
578 let mut count_sql = String::from("SELECT COUNT(*) FROM chat WHERE 1=1");
580 let mut count_binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
581
582 if !params.with_archived {
583 count_sql.push_str(" AND chat.is_archived = 0");
584 }
585 if let Some(ref guid) = params.chat_guid {
586 if params.glob_guid {
587 count_sql.push_str(" AND chat.guid LIKE ?");
588 count_binds.push(Box::new(format!("%{guid}%")));
589 } else {
590 count_sql.push_str(" AND chat.guid = ?");
591 count_binds.push(Box::new(guid.clone()));
592 }
593 }
594
595 let count_refs: Vec<&dyn rusqlite::types::ToSql> =
596 count_binds.iter().map(|b| b.as_ref()).collect();
597 let count: i64 = self
598 .conn
599 .query_row(&count_sql, count_refs.as_slice(), |row| row.get(0))?;
600
601 Ok((chats, count))
602 }
603
604 pub fn get_handles(&self, params: &HandleQueryParams) -> Result<(Vec<Handle>, i64)> {
606 let mut sql = String::from(
607 "SELECT handle.ROWID, handle.id, handle.country, handle.service, \
608 handle.uncanonicalized_id FROM handle",
609 );
610 let mut bind_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
611
612 if let Some(ref address) = params.address {
613 let stripped = address.replace('+', "");
615 sql.push_str(" WHERE handle.id LIKE ?");
616 bind_values.push(Box::new(format!("%{stripped}")));
617 }
618
619 sql.push_str(&format!(" LIMIT {} OFFSET {}", params.limit, params.offset));
620
621 let bind_refs: Vec<&dyn rusqlite::types::ToSql> =
622 bind_values.iter().map(|b| b.as_ref()).collect();
623 let mut stmt = self.conn.prepare(&sql)?;
624 let rows = stmt.query_map(bind_refs.as_slice(), |row| Ok(row_reader::read_handle(row)))?;
625
626 let mut handles: Vec<Handle> = vec![];
627 for row_result in rows {
628 handles.push(row_result?);
629 }
630
631 let count = self.get_handle_count(params.address.as_deref())?;
632
633 Ok((handles, count))
634 }
635
636 pub fn get_attachments_for_message(&self, message_rowid: i64) -> Result<Vec<Attachment>> {
642 let att_cols = self.schema.attachment_select_columns();
643 let att_select = att_cols.join(", ");
644
645 let sql = format!(
646 "SELECT {att_select} FROM attachment \
647 INNER JOIN message_attachment_join ON attachment.ROWID = message_attachment_join.attachment_id \
648 WHERE message_attachment_join.message_id = ?"
649 );
650
651 let mut stmt = self.conn.prepare(&sql)?;
652 let rows = stmt.query_map(params![message_rowid], |row| {
653 Ok(row_reader::read_attachment(row, &self.schema))
654 })?;
655
656 let mut attachments = vec![];
657 for row_result in rows {
658 attachments.push(row_result?);
659 }
660 Ok(attachments)
661 }
662
663 fn get_chats_for_message(&self, message_rowid: i64) -> Result<Vec<Chat>> {
665 let chat_cols = self.schema.chat_select_columns();
666 let chat_select = chat_cols.join(", ");
667
668 let sql = format!(
669 "SELECT {chat_select} FROM chat \
670 INNER JOIN chat_message_join ON chat.ROWID = chat_message_join.chat_id \
671 WHERE chat_message_join.message_id = ?"
672 );
673
674 let mut stmt = self.conn.prepare(&sql)?;
675 let rows = stmt.query_map(params![message_rowid], |row| {
676 Ok(row_reader::read_chat(row, &self.schema))
677 })?;
678
679 let mut chats = vec![];
680 for row_result in rows {
681 chats.push(row_result?);
682 }
683 Ok(chats)
684 }
685
686 fn get_chat_participants(&self, chat_rowid: i64) -> Result<Vec<Handle>> {
689 let mut join_stmt = self
691 .conn
692 .prepare("SELECT handle_id FROM chat_handle_join WHERE chat_id = ?")?;
693 let handle_ids: Vec<i64> = join_stmt
694 .query_map(params![chat_rowid], |row| row.get::<_, i64>(0))?
695 .filter_map(|r| r.ok())
696 .collect();
697
698 if handle_ids.is_empty() {
699 return Ok(vec![]);
700 }
701
702 let placeholders: Vec<String> = handle_ids.iter().map(|_| "?".to_string()).collect();
704 let sql = format!(
705 "SELECT handle.ROWID, handle.id, handle.country, handle.service, \
706 handle.uncanonicalized_id FROM handle WHERE handle.ROWID IN ({})",
707 placeholders.join(",")
708 );
709
710 let mut stmt = self.conn.prepare(&sql)?;
711 let params_refs: Vec<&dyn rusqlite::types::ToSql> = handle_ids
712 .iter()
713 .map(|id| id as &dyn rusqlite::types::ToSql)
714 .collect();
715
716 let rows = stmt.query_map(params_refs.as_slice(), |row| {
717 Ok(row_reader::read_handle(row))
718 })?;
719
720 let mut handle_map: HashMap<i64, Handle> = HashMap::new();
721 for row_result in rows {
722 let h = row_result?;
723 handle_map.insert(h.rowid, h);
724 }
725
726 let handles: Vec<Handle> = handle_ids
728 .iter()
729 .filter_map(|id| handle_map.remove(id))
730 .collect();
731
732 Ok(handles)
733 }
734
735 pub fn get_chats_read_since(&self, after_apple_ts: i64) -> Result<Vec<Chat>> {
742 let chat_cols = self.schema.chat_select_columns();
743 let chat_select = chat_cols.join(", ");
744
745 let sql = format!(
746 "SELECT {chat_select} FROM chat \
747 WHERE chat.last_read_message_timestamp >= ? \
748 ORDER BY chat.last_read_message_timestamp DESC"
749 );
750
751 let mut stmt = self.conn.prepare(&sql)?;
752 let rows = stmt.query_map([after_apple_ts], |row| {
753 Ok(row_reader::read_chat(row, &self.schema))
754 })?;
755
756 let mut chats = Vec::new();
757 for row_result in rows {
758 chats.push(row_result?);
759 }
760 Ok(chats)
761 }
762
763 pub fn get_media_counts(&self, media_type: &str, after: Option<i64>) -> Result<i64> {
765 let mime_prefix = if media_type == "location" {
766 "text/x-vlocation".to_string()
767 } else {
768 media_type.to_string()
769 };
770
771 let (sql, bind_values): (String, Vec<Box<dyn rusqlite::types::ToSql>>) =
772 if let Some(after_ms) = after {
773 let apple_ts = date_to_db(after_ms);
774 (
775 "SELECT COUNT(attachment.ROWID) AS media_count \
776 FROM attachment \
777 WHERE attachment.created_date >= ? AND attachment.mime_type LIKE ?"
778 .to_string(),
779 vec![
780 Box::new(apple_ts) as Box<dyn rusqlite::types::ToSql>,
781 Box::new(format!("{mime_prefix}%")),
782 ],
783 )
784 } else {
785 (
786 "SELECT COUNT(attachment.ROWID) AS media_count \
787 FROM attachment \
788 WHERE attachment.mime_type LIKE ?"
789 .to_string(),
790 vec![Box::new(format!("{mime_prefix}%")) as Box<dyn rusqlite::types::ToSql>],
791 )
792 };
793
794 let bind_refs: Vec<&dyn rusqlite::types::ToSql> =
795 bind_values.iter().map(|b| b.as_ref()).collect();
796 let count: i64 = self
797 .conn
798 .query_row(&sql, bind_refs.as_slice(), |row| row.get(0))?;
799 Ok(count)
800 }
801
802 pub fn get_media_counts_by_chat(&self, chat_guid: &str, media_type: &str) -> Result<i64> {
804 let mime_prefix = if media_type == "location" {
805 "text/x-vlocation".to_string()
806 } else {
807 media_type.to_string()
808 };
809
810 let sql = "SELECT COUNT(attachment.ROWID) AS media_count \
811 FROM attachment \
812 INNER JOIN message_attachment_join ON attachment.ROWID = message_attachment_join.attachment_id \
813 INNER JOIN chat_message_join ON message_attachment_join.message_id = chat_message_join.message_id \
814 INNER JOIN chat ON chat.ROWID = chat_message_join.chat_id \
815 WHERE chat.guid = ? AND attachment.mime_type LIKE ?";
816
817 let count: i64 = self.conn.query_row(
818 sql,
819 rusqlite::params![chat_guid, format!("{mime_prefix}%")],
820 |row| row.get(0),
821 )?;
822 Ok(count)
823 }
824
825 fn append_date_sql(
831 &self,
832 sql: &mut String,
833 bind_values: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
834 after: Option<i64>,
835 before: Option<i64>,
836 ) {
837 sql.push_str(" AND (1=1");
838 if let Some(after_ms) = after {
839 sql.push_str(" AND message.date >= ?");
840 bind_values.push(Box::new(date_to_db(after_ms)));
841 }
842 if let Some(before_ms) = before {
843 sql.push_str(" AND message.date <= ?");
844 bind_values.push(Box::new(date_to_db(before_ms)));
845 }
846 sql.push(')');
847 }
848
849 fn append_update_date_sql(
852 &self,
853 sql: &mut String,
854 bind_values: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
855 after: Option<i64>,
856 before: Option<i64>,
857 include_created: bool,
858 ) {
859 sql.push_str(" AND (");
860 let mut first = true;
861
862 let add_date_clause = |sql: &mut String,
864 bind_values: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
865 col: &str,
866 first: &mut bool| {
867 if !*first {
868 sql.push_str(" OR ");
869 }
870 *first = false;
871 sql.push_str("(1=1");
872 if let Some(after_ms) = after {
873 sql.push_str(&format!(" AND {col} >= ?"));
874 bind_values.push(Box::new(date_to_db(after_ms)));
875 }
876 if let Some(before_ms) = before {
877 sql.push_str(&format!(" AND {col} <= ?"));
878 bind_values.push(Box::new(date_to_db(before_ms)));
879 }
880 sql.push(')');
881 };
882
883 if include_created {
884 add_date_clause(sql, bind_values, "message.date", &mut first);
885 }
886
887 add_date_clause(sql, bind_values, "message.date_delivered", &mut first);
888 add_date_clause(sql, bind_values, "message.date_read", &mut first);
889 add_date_clause(sql, bind_values, "message.date_edited", &mut first);
890 add_date_clause(sql, bind_values, "message.date_retracted", &mut first);
891
892 sql.push(')');
893 }
894}
895
896#[cfg(test)]
897mod tests {
898 use super::*;
899 use serde_json::json;
900
901 #[test]
902 fn expand_simple_named_param() {
903 let mut binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
904 let mut args = HashMap::new();
905 args.insert("term".to_string(), json!("%hello%"));
906 let result =
907 expand_named_params("message.text LIKE :term COLLATE NOCASE", &args, &mut binds);
908 assert_eq!(result, "message.text LIKE ? COLLATE NOCASE");
909 assert_eq!(binds.len(), 1);
910 }
911
912 #[test]
913 fn expand_spread_param() {
914 let mut binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
915 let mut args = HashMap::new();
916 args.insert("guids".to_string(), json!(["g1", "g2", "g3"]));
917 let result = expand_named_params("message.guid IN (:...guids)", &args, &mut binds);
918 assert_eq!(result, "message.guid IN (?, ?, ?)");
919 assert_eq!(binds.len(), 3);
920 }
921
922 #[test]
923 fn expand_multiple_params() {
924 let mut binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
925 let mut args = HashMap::new();
926 args.insert("after".to_string(), json!(100));
927 args.insert("before".to_string(), json!(200));
928 let result = expand_named_params(
929 "message.date >= :after AND message.date <= :before",
930 &args,
931 &mut binds,
932 );
933 assert_eq!(result, "message.date >= ? AND message.date <= ?");
934 assert_eq!(binds.len(), 2);
935 }
936
937 #[test]
938 fn expand_no_params() {
939 let mut binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
940 let args = HashMap::new();
941 let result = expand_named_params("message.is_from_me = 1", &args, &mut binds);
942 assert_eq!(result, "message.is_from_me = 1");
943 assert_eq!(binds.len(), 0);
944 }
945
946 #[test]
947 fn apply_where_clauses_empty() {
948 let mut sql = "SELECT * FROM message WHERE 1=1".to_string();
949 let mut binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
950 apply_where_clauses(&mut sql, &mut binds, &[]);
951 assert_eq!(sql, "SELECT * FROM message WHERE 1=1");
952 }
953
954 #[test]
955 fn apply_where_clauses_adds_and() {
956 let mut sql = "SELECT * FROM message WHERE 1=1".to_string();
957 let mut binds: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
958 let mut args = HashMap::new();
959 args.insert("val".to_string(), json!(1));
960 let clauses = vec![WhereClause {
961 statement: "message.is_from_me = :val".to_string(),
962 args,
963 }];
964 apply_where_clauses(&mut sql, &mut binds, &clauses);
965 assert_eq!(
966 sql,
967 "SELECT * FROM message WHERE 1=1 AND (message.is_from_me = ?)"
968 );
969 assert_eq!(binds.len(), 1);
970 }
971}