1use super::{cleanup_pages, PageToClean};
31use crate::error::Result;
32use crate::{PlacesDb, RowId};
33use rusqlite::Row;
34use sql_support::ConnExt;
35use std::collections::HashSet;
36
37#[derive(Debug, PartialEq, Eq)]
39pub(super) enum DbAction {
40 DeleteVisitRows { visit_ids: HashSet<RowId> },
42 RecalcPages { page_ids: HashSet<RowId> },
45 DeleteFromPendingTempTables,
51}
52
53impl DbAction {
54 pub(super) fn apply(self, db: &PlacesDb) -> Result<()> {
55 match self {
56 Self::DeleteVisitRows { visit_ids } => Self::delete_visit_rows(db, visit_ids),
57 Self::RecalcPages { page_ids } => Self::recalc_pages(db, page_ids),
58 Self::DeleteFromPendingTempTables => Self::delete_from_pending_temp_tables(db),
59 }
60 }
61
62 pub(super) fn apply_all(db: &PlacesDb, actions: Vec<Self>) -> Result<()> {
63 for action in actions {
64 action.apply(db)?;
65 }
66 Ok(())
67 }
68
69 fn delete_visit_rows(db: &PlacesDb, visit_ids: HashSet<RowId>) -> Result<()> {
70 sql_support::each_chunk(&Vec::from_iter(visit_ids), |chunk, _| -> Result<()> {
71 let var_repeat = sql_support::repeat_sql_vars(chunk.len());
72 let params = rusqlite::params_from_iter(chunk);
73 db.execute_cached(
74 &format!(
75 "
76 INSERT OR IGNORE INTO moz_historyvisit_tombstones(place_id, visit_date)
77 SELECT place_id, visit_date
78 FROM moz_historyvisits
79 WHERE id IN ({})
80 ",
81 var_repeat,
82 ),
83 params.clone(),
84 )?;
85
86 db.execute_cached(
87 &format!("DELETE FROM moz_historyvisits WHERE id IN ({})", var_repeat),
88 params,
89 )?;
90 Ok(())
91 })?;
92 Ok(())
93 }
94
95 fn recalc_pages(db: &PlacesDb, page_ids: HashSet<RowId>) -> Result<()> {
96 let mut pages_to_clean: Vec<PageToClean> = vec![];
97 sql_support::each_chunk(&Vec::from_iter(page_ids), |chunk, _| -> Result<()> {
98 pages_to_clean.append(&mut db.query_rows_and_then_cached(
99 &format!(
100 "SELECT
101 id,
102 (foreign_count != 0) AS has_foreign,
103 ((last_visit_date_local + last_visit_date_remote) != 0) as has_visits,
104 sync_status
105 FROM moz_places
106 WHERE id IN ({})",
107 sql_support::repeat_sql_vars(chunk.len())
108 ),
109 rusqlite::params_from_iter(chunk),
110 PageToClean::from_row,
111 )?);
112 Ok(())
113 })?;
114 cleanup_pages(db, &pages_to_clean)?;
115 Ok(())
116 }
117
118 fn delete_from_pending_temp_tables(db: &PlacesDb) -> Result<()> {
119 crate::storage::delete_pending_temp_tables(db)
120 }
121}
122
123#[derive(Debug, PartialEq, Eq, Hash)]
128pub(super) struct VisitToDelete {
129 pub(super) visit_id: RowId,
130 pub(super) page_id: RowId,
131}
132
133impl VisitToDelete {
134 pub(super) fn from_row(row: &Row<'_>) -> Result<Self> {
138 Ok(Self {
139 visit_id: row.get("id")?,
140 page_id: row.get("place_id")?,
141 })
142 }
143}
144
145pub(super) fn db_actions_from_visits_to_delete(
147 visits_to_delete: Vec<VisitToDelete>,
148) -> Vec<DbAction> {
149 let mut visit_ids = HashSet::<RowId>::new();
150 let mut page_ids = HashSet::<RowId>::new();
151 for visit_to_delete in visits_to_delete.into_iter() {
152 visit_ids.insert(visit_to_delete.visit_id);
153 page_ids.insert(visit_to_delete.page_id);
154 }
155 vec![
156 DbAction::DeleteVisitRows { visit_ids },
157 DbAction::RecalcPages { page_ids },
158 DbAction::DeleteFromPendingTempTables,
159 ]
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::observation::VisitObservation;
166 use crate::storage::bookmarks::*;
167 use crate::storage::history::apply_observation;
168 use crate::types::VisitType;
169 use crate::{frecency, ConnectionType, SyncStatus};
170 use rusqlite::params;
171 use rusqlite::types::{FromSql, ToSql};
172 use std::time::Duration;
173 use sync_guid::Guid;
174 use types::Timestamp;
175 use url::Url;
176
177 fn query_vec<T: FromSql>(conn: &PlacesDb, sql: &str, params: &[&dyn ToSql]) -> Vec<T> {
178 conn.prepare(sql)
179 .unwrap()
180 .query_map(params, |row| row.get(0))
181 .unwrap()
182 .collect::<rusqlite::Result<Vec<T>>>()
183 .unwrap()
184 }
185
186 fn query_vec_pairs<T: FromSql, V: FromSql>(
187 conn: &PlacesDb,
188 sql: &str,
189 params: &[&dyn ToSql],
190 ) -> Vec<(T, V)> {
191 conn.prepare(sql)
192 .unwrap()
193 .query_map(params, |row| Ok((row.get(0)?, row.get(1)?)))
194 .unwrap()
195 .collect::<rusqlite::Result<Vec<(T, V)>>>()
196 .unwrap()
197 }
198
199 fn query_visit_ids(conn: &PlacesDb) -> Vec<RowId> {
200 query_vec(conn, "SELECT id FROM moz_historyvisits ORDER BY id", &[])
201 }
202
203 fn query_visit_tombstones(conn: &PlacesDb) -> Vec<(RowId, Timestamp)> {
204 query_vec_pairs(
205 conn,
206 "
207 SELECT place_id, visit_date
208 FROM moz_historyvisit_tombstones
209 ORDER BY place_id, visit_date
210 ",
211 &[],
212 )
213 }
214
215 fn query_page_ids(conn: &PlacesDb) -> Vec<RowId> {
216 query_vec(conn, "SELECT id FROM moz_places ORDER BY id", &[])
217 }
218
219 fn query_page_tombstones(conn: &PlacesDb) -> Vec<Guid> {
220 query_vec(
221 conn,
222 "SELECT guid FROM moz_places_tombstones ORDER BY guid",
223 &[],
224 )
225 }
226
227 struct TestPage {
228 id: RowId,
229 guid: Guid,
230 url: Url,
231 visit_ids: Vec<RowId>,
232 visit_dates: Vec<Timestamp>,
233 }
234
235 impl TestPage {
236 fn new(conn: &mut PlacesDb, url: &str, visit_dates: &[Timestamp]) -> Self {
237 let url = Url::parse(url).unwrap();
238 let mut visit_ids = vec![];
239
240 for date in visit_dates {
241 visit_ids.push(
242 apply_observation(
243 conn,
244 VisitObservation::new(url.clone())
245 .with_visit_type(VisitType::Link)
246 .with_at(*date),
247 )
248 .unwrap()
249 .unwrap(),
250 );
251 }
252
253 let (id, guid) = conn
254 .query_row(
255 "
256 SELECT p.id, p.guid
257 FROM moz_places p
258 JOIN moz_historyvisits v ON p.id = v.place_id
259 WHERE v.id = ?",
260 [visit_ids[0]],
261 |row| Ok((row.get(0)?, row.get(1)?)),
262 )
263 .unwrap();
264
265 Self {
266 id,
267 guid,
268 visit_ids,
269 url,
270 visit_dates: Vec::from_iter(visit_dates.iter().cloned()),
271 }
272 }
273
274 fn set_sync_status(&self, conn: &PlacesDb, sync_status: SyncStatus) {
275 conn.execute(
276 "UPDATE moz_places SET sync_status = ? WHERE id = ?",
277 params! {sync_status, self.id },
278 )
279 .unwrap();
280 }
281
282 fn query_frecency(&self, conn: &PlacesDb) -> i32 {
283 conn.query_row(
284 "SELECT frecency FROM moz_places WHERE id = ?",
285 [self.id],
286 |row| row.get::<usize, i32>(0),
287 )
288 .unwrap()
289 }
290
291 fn calculate_frecency(&self, conn: &PlacesDb) -> i32 {
292 frecency::calculate_frecency(
293 conn,
294 &frecency::DEFAULT_FRECENCY_SETTINGS,
295 self.id.0,
296 None,
297 )
298 .unwrap()
299 }
300
301 fn bookmark(&self, conn: &PlacesDb, title: &str) {
302 insert_bookmark(
303 conn,
304 InsertableBookmark {
305 parent_guid: BookmarkRootGuid::Unfiled.into(),
306 position: BookmarkPosition::Append,
307 date_added: None,
308 last_modified: None,
309 guid: None,
310 url: self.url.clone(),
311 title: Some(title.to_owned()),
312 }
313 .into(),
314 )
315 .unwrap();
316 }
317 }
318
319 #[test]
320 fn test_delete_visit_rows() {
321 let mut conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).unwrap();
322 let yesterday = Timestamp::now()
323 .checked_sub(Duration::from_secs(60 * 60 * 24))
324 .unwrap();
325 let page = TestPage::new(
326 &mut conn,
327 "http://example.com/",
328 &[
329 Timestamp(yesterday.0 + 100),
330 Timestamp(yesterday.0 + 200),
331 Timestamp(yesterday.0 + 300),
332 ],
333 );
334
335 DbAction::DeleteVisitRows {
336 visit_ids: HashSet::from_iter([page.visit_ids[0], page.visit_ids[1]]),
337 }
338 .apply(&conn)
339 .unwrap();
340
341 assert_eq!(query_visit_ids(&conn), vec![page.visit_ids[2]]);
342 assert_eq!(
343 query_visit_tombstones(&conn),
344 vec![
345 (page.id, page.visit_dates[0]),
346 (page.id, page.visit_dates[1]),
347 ]
348 );
349 }
350
351 #[test]
352 fn test_recalc_pages() {
353 let mut conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).unwrap();
354 let yesterday = Timestamp::now()
355 .checked_sub(Duration::from_secs(60 * 60 * 24))
356 .unwrap();
357 let page_with_visits_left = TestPage::new(
358 &mut conn,
359 "http://example.com/1",
360 &[Timestamp(yesterday.0 + 100), Timestamp(yesterday.0 + 200)],
361 );
362 let page_with_no_visits_unsynced = TestPage::new(
363 &mut conn,
364 "http://example.com/2",
365 &[Timestamp(yesterday.0 + 300)],
366 );
367 let page_with_no_visits_synced = TestPage::new(
368 &mut conn,
369 "http://example.com/2",
370 &[Timestamp(yesterday.0 + 400)],
371 );
372 let page_with_no_visits_bookmarked = TestPage::new(
373 &mut conn,
374 "http://example.com/3",
375 &[Timestamp(yesterday.0 + 500)],
376 );
377
378 page_with_no_visits_synced.set_sync_status(&conn, SyncStatus::Normal);
379 page_with_no_visits_bookmarked.bookmark(&conn, "My Bookmark");
380
381 DbAction::DeleteVisitRows {
382 visit_ids: HashSet::from_iter([
383 page_with_visits_left.visit_ids[0],
384 page_with_no_visits_unsynced.visit_ids[0],
385 page_with_no_visits_synced.visit_ids[0],
386 page_with_no_visits_bookmarked.visit_ids[0],
387 ]),
388 }
389 .apply(&conn)
390 .unwrap();
391
392 DbAction::RecalcPages {
393 page_ids: HashSet::from_iter([
394 page_with_visits_left.id,
395 page_with_no_visits_unsynced.id,
396 page_with_no_visits_synced.id,
397 page_with_no_visits_bookmarked.id,
398 ]),
399 }
400 .apply(&conn)
401 .unwrap();
402
403 assert_eq!(
404 query_page_ids(&conn),
405 [page_with_visits_left.id, page_with_no_visits_bookmarked.id]
406 );
407 assert_eq!(
408 query_page_tombstones(&conn),
409 [page_with_no_visits_synced.guid]
410 );
411 assert_eq!(
412 page_with_visits_left.query_frecency(&conn),
413 page_with_visits_left.calculate_frecency(&conn)
414 );
415 assert_eq!(
416 page_with_no_visits_bookmarked.query_frecency(&conn),
417 page_with_no_visits_bookmarked.calculate_frecency(&conn)
418 );
419 }
420
421 #[test]
422 fn test_delete_from_pending_temp_tables() {
423 let mut conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).unwrap();
424 let yesterday = Timestamp::now()
425 .checked_sub(Duration::from_secs(60 * 60 * 24))
426 .unwrap();
427 let test_page = TestPage::new(
428 &mut conn,
429 "http://example.com/",
430 &[
431 Timestamp(yesterday.0 + 100),
432 Timestamp(yesterday.0 + 200),
433 Timestamp(yesterday.0 + 300),
434 ],
435 );
436 DbAction::DeleteVisitRows {
437 visit_ids: HashSet::from_iter([test_page.visit_ids[0]]),
438 }
439 .apply(&conn)
440 .unwrap();
441 DbAction::RecalcPages {
442 page_ids: HashSet::from_iter([test_page.id]),
443 }
444 .apply(&conn)
445 .unwrap();
446 DbAction::DeleteFromPendingTempTables.apply(&conn).unwrap();
447 assert_eq!(
448 conn.conn_ext_query_one::<u32>("SELECT COUNT(*) FROM moz_updateoriginsinsert_temp")
449 .unwrap(),
450 0
451 );
452 assert_eq!(
453 conn.conn_ext_query_one::<u32>("SELECT COUNT(*) FROM moz_updateoriginsupdate_temp")
454 .unwrap(),
455 0
456 );
457 assert_eq!(
458 conn.conn_ext_query_one::<u32>("SELECT COUNT(*) FROM moz_updateoriginsdelete_temp")
459 .unwrap(),
460 0
461 );
462 }
463
464 #[test]
465 fn test_db_actions_from_visits_to_delete() {
466 assert_eq!(
467 db_actions_from_visits_to_delete(vec![
468 VisitToDelete {
469 visit_id: RowId(1),
470 page_id: RowId(1),
471 },
472 VisitToDelete {
473 visit_id: RowId(2),
474 page_id: RowId(2),
475 },
476 VisitToDelete {
477 visit_id: RowId(3),
478 page_id: RowId(2),
479 },
480 ]),
481 vec![
482 DbAction::DeleteVisitRows {
483 visit_ids: HashSet::from_iter([RowId(1), RowId(2), RowId(3)])
484 },
485 DbAction::RecalcPages {
486 page_ids: HashSet::from_iter([RowId(1), RowId(2)])
487 },
488 DbAction::DeleteFromPendingTempTables,
489 ],
490 )
491 }
492}