1use crate::db::{PlacesDb, PlacesTransaction};
6use crate::error::*;
7use crate::RowId;
8use error_support::{breadcrumb, redact_url};
9use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
10use sql_support::ConnExt;
11use std::vec::Vec;
12use sync_guid::Guid as SyncGuid;
13use types::Timestamp;
14use url::Url;
15
16use lazy_static::lazy_static;
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub enum DocumentType {
20 Regular = 0,
21 Media = 1,
22}
23
24impl FromSql for DocumentType {
25 #[inline]
26 fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
27 Ok(match value.as_i64()? {
28 0 => DocumentType::Regular,
29 1 => DocumentType::Media,
30 other => {
31 warn!("invalid DocumentType {}", other);
33 DocumentType::Regular
34 }
35 })
36 }
37}
38
39impl ToSql for DocumentType {
40 #[inline]
41 fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
42 Ok(ToSqlOutput::from(*self as u32))
43 }
44}
45
46#[derive(Clone)]
47pub struct HistoryHighlightWeights {
48 pub view_time: f64,
49 pub frequency: f64,
50}
51
52#[derive(Clone)]
53pub struct HistoryHighlight {
54 pub score: f64,
55 pub place_id: i32,
56 pub url: String,
57 pub title: Option<String>,
58 pub preview_image_url: Option<String>,
59}
60
61impl HistoryHighlight {
62 pub(crate) fn from_row(row: &rusqlite::Row<'_>) -> Result<Self> {
63 Ok(Self {
64 score: row.get("score")?,
65 place_id: row.get("place_id")?,
66 url: row.get("url")?,
67 title: row.get("title")?,
68 preview_image_url: row.get("preview_image_url")?,
69 })
70 }
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct HistoryMetadataObservation {
75 pub url: String,
76 pub view_time: Option<i32>,
77 pub search_term: Option<String>,
78 pub document_type: Option<DocumentType>,
79 pub referrer_url: Option<String>,
80 pub title: Option<String>,
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum HistoryMetadataPageMissingBehavior {
85 InsertPage,
86 IgnoreObservation,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct NoteHistoryMetadataObservationOptions {
91 pub if_page_missing: HistoryMetadataPageMissingBehavior,
92}
93
94impl Default for NoteHistoryMetadataObservationOptions {
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100impl NoteHistoryMetadataObservationOptions {
101 pub fn new() -> Self {
102 Self {
103 if_page_missing: HistoryMetadataPageMissingBehavior::IgnoreObservation,
104 }
105 }
106
107 pub fn if_page_missing(self, if_page_missing: HistoryMetadataPageMissingBehavior) -> Self {
108 Self { if_page_missing }
109 }
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct HistoryMetadata {
114 pub url: String,
115 pub title: Option<String>,
116 pub preview_image_url: Option<String>,
117 pub created_at: i64,
118 pub updated_at: i64,
119 pub total_view_time: i32,
120 pub search_term: Option<String>,
121 pub document_type: DocumentType,
122 pub referrer_url: Option<String>,
123}
124
125impl HistoryMetadata {
126 pub(crate) fn from_row(row: &rusqlite::Row<'_>) -> Result<Self> {
127 let created_at: Timestamp = row.get("created_at")?;
128 let updated_at: Timestamp = row.get("updated_at")?;
129
130 let total_view_time: i64 = row.get("total_view_time")?;
137 let total_view_time = match i32::try_from(total_view_time) {
138 Ok(tvt) => tvt,
139 Err(_) => i32::MAX,
140 };
141
142 Ok(Self {
143 url: row.get("url")?,
144 title: row.get("title")?,
145 preview_image_url: row.get("preview_image_url")?,
146 created_at: created_at.0 as i64,
147 updated_at: updated_at.0 as i64,
148 total_view_time,
149 search_term: row.get("search_term")?,
150 document_type: row.get("document_type")?,
151 referrer_url: row.get("referrer_url")?,
152 })
153 }
154}
155
156enum PlaceEntry {
157 Existing(i64),
158 CreateFor(Url, Option<String>),
159}
160
161trait WhereArg {
162 fn to_where_arg(&self, db_field: &str) -> String;
163}
164
165impl PlaceEntry {
166 fn fetch(url: &str, tx: &PlacesTransaction<'_>, title: Option<String>) -> Result<Self> {
167 let url = Url::parse(url).inspect_err(|_e| {
168 breadcrumb!(
169 "PlaceEntry::fetch -- Error parsing url: {}",
170 redact_url(url)
171 );
172 })?;
173 let place_id = tx.try_query_one(
174 "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
175 &[(":url", &url.as_str())],
176 true,
177 )?;
178
179 Ok(match place_id {
180 Some(id) => PlaceEntry::Existing(id),
181 None => PlaceEntry::CreateFor(url, title),
182 })
183 }
184}
185
186impl WhereArg for PlaceEntry {
187 fn to_where_arg(&self, db_field: &str) -> String {
188 match self {
189 PlaceEntry::Existing(id) => format!("{} = {}", db_field, id),
190 PlaceEntry::CreateFor(_, _) => panic!("WhereArg: place entry must exist"),
191 }
192 }
193}
194
195impl WhereArg for Option<PlaceEntry> {
196 fn to_where_arg(&self, db_field: &str) -> String {
197 match self {
198 Some(entry) => entry.to_where_arg(db_field),
199 None => format!("{} IS NULL", db_field),
200 }
201 }
202}
203
204trait DatabaseId {
205 fn get_or_insert(&self, tx: &PlacesTransaction<'_>) -> Result<i64>;
206}
207
208impl DatabaseId for PlaceEntry {
209 fn get_or_insert(&self, tx: &PlacesTransaction<'_>) -> Result<i64> {
210 Ok(match self {
211 PlaceEntry::Existing(id) => *id,
212 PlaceEntry::CreateFor(url, title) => {
213 let sql = "INSERT INTO moz_places (guid, url, title, url_hash)
214 VALUES (:guid, :url, :title, hash(:url))";
215
216 let guid = SyncGuid::random();
217
218 tx.execute_cached(
219 sql,
220 &[
221 (":guid", &guid as &dyn rusqlite::ToSql),
222 (":title", &title),
223 (":url", &url.as_str()),
224 ],
225 )?;
226 tx.conn().last_insert_rowid()
227 }
228 })
229 }
230}
231
232enum SearchQueryEntry {
233 Existing(i64),
234 CreateFor(String),
235}
236
237impl DatabaseId for SearchQueryEntry {
238 fn get_or_insert(&self, tx: &PlacesTransaction<'_>) -> Result<i64> {
239 Ok(match self {
240 SearchQueryEntry::Existing(id) => *id,
241 SearchQueryEntry::CreateFor(term) => {
242 tx.execute_cached(
243 "INSERT INTO moz_places_metadata_search_queries(term) VALUES (:term)",
244 &[(":term", &term)],
245 )?;
246 tx.conn().last_insert_rowid()
247 }
248 })
249 }
250}
251
252impl SearchQueryEntry {
253 fn from(search_term: &str, tx: &PlacesTransaction<'_>) -> Result<Self> {
254 let lowercase_term = search_term.to_lowercase();
255 Ok(
256 match tx.try_query_one(
257 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
258 &[(":term", &lowercase_term)],
259 true,
260 )? {
261 Some(id) => SearchQueryEntry::Existing(id),
262 None => SearchQueryEntry::CreateFor(lowercase_term),
263 },
264 )
265 }
266}
267
268impl WhereArg for SearchQueryEntry {
269 fn to_where_arg(&self, db_field: &str) -> String {
270 match self {
271 SearchQueryEntry::Existing(id) => format!("{} = {}", db_field, id),
272 SearchQueryEntry::CreateFor(_) => panic!("WhereArg: search query entry must exist"),
273 }
274 }
275}
276
277impl WhereArg for Option<SearchQueryEntry> {
278 fn to_where_arg(&self, db_field: &str) -> String {
279 match self {
280 Some(entry) => entry.to_where_arg(db_field),
281 None => format!("{} IS NULL", db_field),
282 }
283 }
284}
285
286struct HistoryMetadataCompoundKey {
287 place_entry: PlaceEntry,
288 referrer_entry: Option<PlaceEntry>,
289 search_query_entry: Option<SearchQueryEntry>,
290}
291
292struct MetadataObservation {
293 document_type: Option<DocumentType>,
294 view_time: Option<i32>,
295}
296
297impl HistoryMetadataCompoundKey {
298 fn can_debounce(&self) -> Option<i64> {
299 match self.place_entry {
300 PlaceEntry::Existing(id) => {
301 if (match self.search_query_entry {
302 None | Some(SearchQueryEntry::Existing(_)) => true,
303 Some(SearchQueryEntry::CreateFor(_)) => false,
304 } && match self.referrer_entry {
305 None | Some(PlaceEntry::Existing(_)) => true,
306 Some(PlaceEntry::CreateFor(_, _)) => false,
307 }) {
308 Some(id)
309 } else {
310 None
311 }
312 }
313 _ => None,
314 }
315 }
316
317 fn lookup(&self, tx: &PlacesTransaction<'_>, newer_than: i64) -> Result<Option<i64>> {
319 Ok(match self.can_debounce() {
320 Some(id) => {
321 let search_query_id = match self.search_query_entry {
322 None | Some(SearchQueryEntry::CreateFor(_)) => None,
323 Some(SearchQueryEntry::Existing(id)) => Some(id),
324 };
325
326 let referrer_place_id = match self.referrer_entry {
327 None | Some(PlaceEntry::CreateFor(_, _)) => None,
328 Some(PlaceEntry::Existing(id)) => Some(id),
329 };
330
331 tx.try_query_one::<i64, _>(
332 "SELECT id FROM moz_places_metadata
333 WHERE
334 place_id IS :place_id AND
335 referrer_place_id IS :referrer_place_id AND
336 search_query_id IS :search_query_id AND
337 updated_at >= :newer_than
338 ORDER BY updated_at DESC LIMIT 1",
339 rusqlite::named_params! {
340 ":place_id": id,
341 ":search_query_id": search_query_id,
342 ":referrer_place_id": referrer_place_id,
343 ":newer_than": newer_than
344 },
345 true,
346 )?
347 }
348 None => None,
349 })
350 }
351}
352
353const DEBOUNCE_WINDOW_MS: i64 = 2 * 60 * 1000; const MAX_QUERY_RESULTS: i32 = 1000;
355
356const COMMON_METADATA_SELECT: &str = "
357SELECT
358 m.id as metadata_id, p.url as url, p.title as title, p.preview_image_url as preview_image_url,
359 m.created_at as created_at, m.updated_at as updated_at, m.total_view_time as total_view_time,
360 m.document_type as document_type, o.url as referrer_url, s.term as search_term
361FROM moz_places_metadata m
362LEFT JOIN moz_places p ON m.place_id = p.id
363LEFT JOIN moz_places_metadata_search_queries s ON m.search_query_id = s.id
364LEFT JOIN moz_places o ON o.id = m.referrer_place_id";
365
366const HIGHLIGHTS_QUERY: &str = "
403SELECT
404 IFNULL(ranked.score, 0.0) AS score, p.id AS place_id, p.url AS url, p.title AS title, p.preview_image_url AS preview_image_url
405FROM moz_places p
406INNER JOIN
407 (
408 SELECT place_id, :view_time_weight * view_time_prob + :frequency_weight * frequency_prob AS score FROM (
409 SELECT
410 place_id,
411 CAST(count(*) AS REAL) / total_count AS frequency_prob,
412 CAST(sum(total_view_time) AS REAL) / all_view_time AS view_time_prob
413 FROM (
414 SELECT place_id, count(*) OVER () AS total_count, total_view_time, sum(total_view_time) OVER () AS all_view_time FROM moz_places_metadata
415 )
416 GROUP BY place_id
417 )
418 ) ranked
419ON p.id = ranked.place_id
420ORDER BY ranked.score DESC
421LIMIT :limit";
422
423lazy_static! {
424 static ref GET_LATEST_SQL: String = format!(
425 "{common_select_sql}
426 WHERE p.url_hash = hash(:url) AND p.url = :url
427 ORDER BY updated_at DESC, metadata_id DESC
428 LIMIT 1",
429 common_select_sql = COMMON_METADATA_SELECT
430 );
431 static ref GET_BETWEEN_SQL: String = format!(
432 "{common_select_sql}
433 WHERE updated_at BETWEEN :start AND :end
434 ORDER BY updated_at DESC
435 LIMIT {max_limit}",
436 common_select_sql = COMMON_METADATA_SELECT,
437 max_limit = MAX_QUERY_RESULTS
438 );
439 static ref GET_SINCE_SQL: String = format!(
440 "{common_select_sql}
441 WHERE updated_at >= :start
442 ORDER BY updated_at DESC
443 LIMIT :limit",
444 common_select_sql = COMMON_METADATA_SELECT
445 );
446 static ref SEARCH_QUERY_SQL: String = format!(
447 "{common_select_sql}
448 WHERE search_term NOT NULL
449 ORDER BY updated_at DESC
450 LIMIT :limit",
451 common_select_sql = COMMON_METADATA_SELECT
452 );
453 static ref QUERY_SQL: String = format!(
454 "{common_select_sql}
455 WHERE
456 p.url LIKE :query OR
457 p.title LIKE :query OR
458 search_term LIKE :query
459 ORDER BY total_view_time DESC
460 LIMIT :limit",
461 common_select_sql = COMMON_METADATA_SELECT
462 );
463}
464
465pub fn get_latest_for_url(db: &PlacesDb, url: &Url) -> Result<Option<HistoryMetadata>> {
466 let metadata = db.try_query_row(
467 GET_LATEST_SQL.as_str(),
468 &[(":url", &url.as_str())],
469 HistoryMetadata::from_row,
470 true,
471 )?;
472 Ok(metadata)
473}
474
475pub fn get_between(db: &PlacesDb, start: i64, end: i64) -> Result<Vec<HistoryMetadata>> {
476 db.query_rows_and_then_cached(
477 GET_BETWEEN_SQL.as_str(),
478 rusqlite::named_params! {
479 ":start": start,
480 ":end": end,
481 },
482 HistoryMetadata::from_row,
483 )
484}
485
486pub fn get_since(db: &PlacesDb, start: i64) -> Result<Vec<HistoryMetadata>> {
491 db.query_rows_and_then_cached(
492 GET_SINCE_SQL.as_str(),
493 rusqlite::named_params! {
494 ":start": start,
495 ":limit": MAX_QUERY_RESULTS,
496 },
497 HistoryMetadata::from_row,
498 )
499}
500
501pub fn get_most_recent(db: &PlacesDb, limit: i32) -> Result<Vec<HistoryMetadata>> {
507 db.query_rows_and_then_cached(
508 GET_SINCE_SQL.as_str(),
509 rusqlite::named_params! {
510 ":start": i64::MIN,
511 ":limit": limit,
512 },
513 HistoryMetadata::from_row,
514 )
515}
516
517pub fn get_most_recent_search_entries(db: &PlacesDb, limit: i32) -> Result<Vec<HistoryMetadata>> {
522 db.query_rows_and_then_cached(
523 SEARCH_QUERY_SQL.as_str(),
524 rusqlite::named_params! {
525 ":limit": limit,
526 },
527 HistoryMetadata::from_row,
528 )
529}
530
531pub fn get_highlights(
532 db: &PlacesDb,
533 weights: HistoryHighlightWeights,
534 limit: i32,
535) -> Result<Vec<HistoryHighlight>> {
536 db.query_rows_and_then_cached(
537 HIGHLIGHTS_QUERY,
538 rusqlite::named_params! {
539 ":view_time_weight": weights.view_time,
540 ":frequency_weight": weights.frequency,
541 ":limit": limit
542 },
543 HistoryHighlight::from_row,
544 )
545}
546
547pub fn query(db: &PlacesDb, query: &str, limit: i32) -> Result<Vec<HistoryMetadata>> {
548 db.query_rows_and_then_cached(
549 QUERY_SQL.as_str(),
550 rusqlite::named_params! {
551 ":query": format!("%{}%", query),
552 ":limit": limit
553 },
554 HistoryMetadata::from_row,
555 )
556}
557
558pub fn delete_older_than(db: &PlacesDb, older_than: i64) -> Result<()> {
559 db.execute_cached(
560 "DELETE FROM moz_places_metadata
561 WHERE updated_at < :older_than",
562 &[(":older_than", &older_than)],
563 )?;
564 Ok(())
565}
566
567pub fn delete_between(db: &PlacesDb, start: i64, end: i64) -> Result<()> {
568 db.execute_cached(
569 "DELETE FROM moz_places_metadata
570 WHERE updated_at > :start and updated_at < :end",
571 &[(":start", &start), (":end", &end)],
572 )?;
573 Ok(())
574}
575
576pub fn delete_all_metadata_for_page(db: &PlacesDb, place_id: RowId) -> Result<()> {
578 db.execute_cached(
579 "DELETE FROM moz_places_metadata
580 WHERE place_id = :place_id",
581 &[(":place_id", &place_id)],
582 )?;
583 Ok(())
584}
585
586pub fn delete_all_metadata_for_search(db: &PlacesDb) -> Result<()> {
588 db.execute_cached("DELETE FROM moz_places_metadata_search_queries", [])?;
589 Ok(())
590}
591
592pub fn delete_metadata(
593 db: &PlacesDb,
594 url: &Url,
595 referrer_url: Option<&Url>,
596 search_term: Option<&str>,
597) -> Result<()> {
598 let tx = db.begin_transaction()?;
599
600 let place_entry = PlaceEntry::fetch(url.as_str(), &tx, None)?;
606 let place_entry = match place_entry {
607 PlaceEntry::Existing(_) => place_entry,
608 PlaceEntry::CreateFor(_, _) => {
609 tx.rollback()?;
610 return Ok(());
611 }
612 };
613 let referrer_entry = match referrer_url {
614 Some(referrer_url) if !referrer_url.as_str().is_empty() => {
615 Some(PlaceEntry::fetch(referrer_url.as_str(), &tx, None)?)
616 }
617 _ => None,
618 };
619 let referrer_entry = match referrer_entry {
620 Some(PlaceEntry::Existing(_)) | None => referrer_entry,
621 Some(PlaceEntry::CreateFor(_, _)) => {
622 tx.rollback()?;
623 return Ok(());
624 }
625 };
626 let search_query_entry = match search_term {
627 Some(search_term) if !search_term.is_empty() => {
628 Some(SearchQueryEntry::from(search_term, &tx)?)
629 }
630 _ => None,
631 };
632 let search_query_entry = match search_query_entry {
633 Some(SearchQueryEntry::Existing(_)) | None => search_query_entry,
634 Some(SearchQueryEntry::CreateFor(_)) => {
635 tx.rollback()?;
636 return Ok(());
637 }
638 };
639
640 let sql = format!(
641 "DELETE FROM moz_places_metadata WHERE {} AND {} AND {}",
642 place_entry.to_where_arg("place_id"),
643 referrer_entry.to_where_arg("referrer_place_id"),
644 search_query_entry.to_where_arg("search_query_id")
645 );
646
647 tx.execute_cached(&sql, [])?;
648 tx.commit()?;
649
650 Ok(())
651}
652
653pub fn apply_metadata_observation(
654 db: &PlacesDb,
655 observation: HistoryMetadataObservation,
656 options: NoteHistoryMetadataObservationOptions,
657) -> Result<()> {
658 if let Some(view_time) = observation.view_time {
659 if view_time > 1000 * 60 * 60 * 24 {
668 return Err(InvalidMetadataObservation::ViewTimeTooLong.into());
669 }
670 }
671
672 let tx = db.begin_transaction()?;
677
678 let place_entry = PlaceEntry::fetch(&observation.url, &tx, observation.title.clone())?;
679 let result = apply_metadata_observation_impl(&tx, place_entry, observation, options);
680
681 super::delete_pending_temp_tables(db)?;
684 match result {
685 Ok(_) => tx.commit()?,
686 Err(_) => tx.rollback()?,
687 };
688
689 result
690}
691
692fn apply_metadata_observation_impl(
693 tx: &PlacesTransaction<'_>,
694 place_entry: PlaceEntry,
695 observation: HistoryMetadataObservation,
696 options: NoteHistoryMetadataObservationOptions,
697) -> Result<()> {
698 let referrer_entry = match observation.referrer_url {
699 Some(referrer_url) if !referrer_url.is_empty() => {
700 Some(PlaceEntry::fetch(&referrer_url, tx, None)?)
701 }
702 Some(_) | None => None,
703 };
704 let search_query_entry = match observation.search_term {
705 Some(search_term) if !search_term.is_empty() => {
706 Some(SearchQueryEntry::from(&search_term, tx)?)
707 }
708 Some(_) | None => None,
709 };
710
711 let compound_key = HistoryMetadataCompoundKey {
712 place_entry,
713 referrer_entry,
714 search_query_entry,
715 };
716
717 let observation = MetadataObservation {
718 document_type: observation.document_type,
719 view_time: observation.view_time,
720 };
721
722 let now = Timestamp::now().as_millis() as i64;
723 let newer_than = now - DEBOUNCE_WINDOW_MS;
724 let matching_metadata = compound_key.lookup(tx, newer_than)?;
725
726 match matching_metadata {
728 Some(metadata_id) => {
729 match observation {
731 MetadataObservation {
732 document_type: Some(dt),
733 view_time,
734 } => {
735 tx.execute_cached(
736 "UPDATE
737 moz_places_metadata
738 SET
739 document_type = :document_type,
740 total_view_time = total_view_time + :view_time_delta,
741 updated_at = :updated_at
742 WHERE id = :id",
743 rusqlite::named_params! {
744 ":id": metadata_id,
745 ":document_type": dt,
746 ":view_time_delta": view_time.unwrap_or(0),
747 ":updated_at": now
748 },
749 )?;
750 }
751 MetadataObservation {
752 document_type: None,
753 view_time,
754 } => {
755 tx.execute_cached(
756 "UPDATE
757 moz_places_metadata
758 SET
759 total_view_time = total_view_time + :view_time_delta,
760 updated_at = :updated_at
761 WHERE id = :id",
762 rusqlite::named_params! {
763 ":id": metadata_id,
764 ":view_time_delta": view_time.unwrap_or(0),
765 ":updated_at": now
766 },
767 )?;
768 }
769 }
770 Ok(())
771 }
772 None => insert_metadata_in_tx(tx, compound_key, observation, options),
773 }
774}
775
776fn insert_metadata_in_tx(
777 tx: &PlacesTransaction<'_>,
778 key: HistoryMetadataCompoundKey,
779 observation: MetadataObservation,
780 options: NoteHistoryMetadataObservationOptions,
781) -> Result<()> {
782 let now = Timestamp::now();
783
784 let referrer_place_id = match key.referrer_entry {
785 None => None,
786 Some(entry) => Some(entry.get_or_insert(tx)?),
787 };
788
789 let search_query_id = match key.search_query_entry {
790 None => None,
791 Some(entry) => Some(entry.get_or_insert(tx)?),
792 };
793
794 let place_id = match (key.place_entry, options.if_page_missing) {
797 (PlaceEntry::Existing(id), _) => id,
798 (PlaceEntry::CreateFor(_, _), HistoryMetadataPageMissingBehavior::IgnoreObservation) => {
799 return Ok(())
800 }
801 (
802 ref entry @ PlaceEntry::CreateFor(_, _),
803 HistoryMetadataPageMissingBehavior::InsertPage,
804 ) => entry.get_or_insert(tx)?,
805 };
806
807 let sql = "INSERT INTO moz_places_metadata
808 (place_id, created_at, updated_at, total_view_time, search_query_id, document_type, referrer_place_id)
809 VALUES
810 (:place_id, :created_at, :updated_at, :total_view_time, :search_query_id, :document_type, :referrer_place_id)";
811
812 tx.execute_cached(
813 sql,
814 &[
815 (":place_id", &place_id as &dyn rusqlite::ToSql),
816 (":created_at", &now),
817 (":updated_at", &now),
818 (":search_query_id", &search_query_id),
819 (":referrer_place_id", &referrer_place_id),
820 (
821 ":document_type",
822 &observation.document_type.unwrap_or(DocumentType::Regular),
823 ),
824 (":total_view_time", &observation.view_time.unwrap_or(0)),
825 ],
826 )?;
827
828 Ok(())
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use crate::api::places_api::ConnectionType;
835 use crate::observation::VisitObservation;
836 use crate::storage::bookmarks::{
837 get_raw_bookmark, insert_bookmark, BookmarkPosition, BookmarkRootGuid, InsertableBookmark,
838 InsertableItem,
839 };
840 use crate::storage::fetch_page_info;
841 use crate::storage::history::{
842 apply_observation, delete_everything, delete_visits_between, delete_visits_for,
843 get_visit_count, url_to_guid,
844 };
845 use crate::types::VisitType;
846 use crate::VisitTransitionSet;
847 use std::{thread, time};
848
849 fn bump_clock() {
854 thread::sleep(time::Duration::from_millis(10));
855 }
856
857 macro_rules! assert_table_size {
858 ($conn:expr, $table:expr, $count:expr) => {
859 assert_eq!(
860 $count,
861 $conn
862 .try_query_one::<i64, _>(
863 format!("SELECT count(*) FROM {table}", table = $table).as_str(),
864 [],
865 true
866 )
867 .expect("select works")
868 .expect("got count")
869 );
870 };
871 }
872
873 macro_rules! assert_history_metadata_record {
874 ($record:expr, url $url:expr, total_time $tvt:expr, search_term $search_term:expr, document_type $document_type:expr, referrer_url $referrer_url:expr, title $title:expr, preview_image_url $preview_image_url:expr) => {
875 assert_eq!(String::from($url), $record.url, "url must match");
876 assert_eq!($tvt, $record.total_view_time, "total_view_time must match");
877 assert_eq!($document_type, $record.document_type, "is_media must match");
878
879 let meta = $record.clone(); match $search_term as Option<&str> {
882 Some(t) => assert_eq!(
883 String::from(t),
884 meta.search_term.expect("search_term must be Some"),
885 "search_term must match"
886 ),
887 None => assert_eq!(
888 true,
889 meta.search_term.is_none(),
890 "search_term expected to be None"
891 ),
892 };
893 match $referrer_url as Option<&str> {
894 Some(t) => assert_eq!(
895 String::from(t),
896 meta.referrer_url.expect("referrer_url must be Some"),
897 "referrer_url must match"
898 ),
899 None => assert_eq!(
900 true,
901 meta.referrer_url.is_none(),
902 "referrer_url expected to be None"
903 ),
904 };
905 match $title as Option<&str> {
906 Some(t) => assert_eq!(
907 String::from(t),
908 meta.title.expect("title must be Some"),
909 "title must match"
910 ),
911 None => assert_eq!(true, meta.title.is_none(), "title expected to be None"),
912 };
913 match $preview_image_url as Option<&str> {
914 Some(t) => assert_eq!(
915 String::from(t),
916 meta.preview_image_url
917 .expect("preview_image_url must be Some"),
918 "preview_image_url must match"
919 ),
920 None => assert_eq!(
921 true,
922 meta.preview_image_url.is_none(),
923 "preview_image_url expected to be None"
924 ),
925 };
926 };
927 }
928
929 macro_rules! assert_total_after_observation {
930 ($conn:expr, total_records_after $total_records:expr, total_view_time_after $total_view_time:expr, url $url:expr, view_time $view_time:expr, search_term $search_term:expr, document_type $document_type:expr, referrer_url $referrer_url:expr, title $title:expr) => {
931 note_observation!($conn,
932 url $url,
933 view_time $view_time,
934 search_term $search_term,
935 document_type $document_type,
936 referrer_url $referrer_url,
937 title $title
938 );
939
940 assert_table_size!($conn, "moz_places_metadata", $total_records);
941 let updated = get_latest_for_url($conn, &Url::parse($url).unwrap()).unwrap().unwrap();
942 assert_eq!($total_view_time, updated.total_view_time, "total view time must match");
943 }
944 }
945
946 macro_rules! note_observation {
947 ($conn:expr, url $url:expr, view_time $view_time:expr, search_term $search_term:expr, document_type $document_type:expr, referrer_url $referrer_url:expr, title $title:expr) => {
948 note_observation!(
949 $conn,
950 NoteHistoryMetadataObservationOptions::new()
951 .if_page_missing(HistoryMetadataPageMissingBehavior::InsertPage),
952 url $url,
953 view_time $view_time,
954 search_term $search_term,
955 document_type $document_type,
956 referrer_url $referrer_url,
957 title $title
958 )
959 };
960 ($conn:expr, $options:expr, url $url:expr, view_time $view_time:expr, search_term $search_term:expr, document_type $document_type:expr, referrer_url $referrer_url:expr, title $title:expr) => {
961 apply_metadata_observation(
962 $conn,
963 HistoryMetadataObservation {
964 url: String::from($url),
965 view_time: $view_time,
966 search_term: $search_term.map(|s: &str| s.to_string()),
967 document_type: $document_type,
968 referrer_url: $referrer_url.map(|s: &str| s.to_string()),
969 title: $title.map(|s: &str| s.to_string()),
970 },
971 $options,
972 )
973 .unwrap();
974 };
975 }
976
977 macro_rules! assert_after_observation {
978 ($conn:expr, total_records_after $total_records:expr, total_view_time_after $total_view_time:expr, url $url:expr, view_time $view_time:expr, search_term $search_term:expr, document_type $document_type:expr, referrer_url $referrer_url:expr, title $title:expr, assertion $assertion:expr) => {
979 assert_total_after_observation!($conn,
981 total_records_after $total_records,
982 total_view_time_after $total_view_time,
983 url $url,
984 view_time $view_time,
985 search_term $search_term,
986 document_type $document_type,
987 referrer_url $referrer_url,
988 title $title
989 );
990
991 let m = get_latest_for_url(
992 $conn,
993 &Url::parse(&String::from($url)).unwrap(),
994 )
995 .unwrap()
996 .unwrap();
997 #[allow(clippy::redundant_closure_call)]
998 $assertion(m);
999 }
1000 }
1001
1002 #[test]
1003 fn test_note_observation() {
1004 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).unwrap();
1005
1006 assert_table_size!(&conn, "moz_places_metadata", 0);
1007
1008 assert_total_after_observation!(&conn,
1009 total_records_after 1,
1010 total_view_time_after 1500,
1011 url "http://mozilla.com/",
1012 view_time Some(1500),
1013 search_term None,
1014 document_type Some(DocumentType::Regular),
1015 referrer_url None,
1016 title None
1017 );
1018
1019 assert_total_after_observation!(&conn,
1021 total_records_after 1,
1022 total_view_time_after 2500,
1023 url "http://mozilla.com/",
1024 view_time Some(1000),
1025 search_term None,
1026 document_type Some(DocumentType::Regular),
1027 referrer_url None,
1028 title None
1029 );
1030
1031 assert_total_after_observation!(&conn,
1033 total_records_after 1,
1034 total_view_time_after 3500,
1035 url "http://mozilla.com/",
1036 view_time Some(1000),
1037 search_term None,
1038 document_type Some(DocumentType::Media),
1039 referrer_url None,
1040 title None
1041 );
1042
1043 assert_total_after_observation!(&conn,
1045 total_records_after 2,
1046 total_view_time_after 2000,
1047 url "http://mozilla.com/",
1048 view_time Some(2000),
1049 search_term None,
1050 document_type Some(DocumentType::Media),
1051 referrer_url Some("https://news.website"),
1052 title None
1053 );
1054
1055 assert_total_after_observation!(&conn,
1057 total_records_after 3,
1058 total_view_time_after 1100,
1059 url "http://mozilla.com/",
1060 view_time Some(1100),
1061 search_term Some("firefox"),
1062 document_type Some(DocumentType::Media),
1063 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=firefox"),
1064 title None
1065 );
1066
1067 assert_total_after_observation!(&conn,
1069 total_records_after 3,
1070 total_view_time_after 6100,
1071 url "http://mozilla.com/",
1072 view_time Some(5000),
1073 search_term Some("firefox"),
1074 document_type Some(DocumentType::Media),
1075 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=firefox"),
1076 title None
1077 );
1078
1079 assert_total_after_observation!(&conn,
1081 total_records_after 4,
1082 total_view_time_after 3000,
1083 url "http://mozilla.com/another",
1084 view_time Some(3000),
1085 search_term None,
1086 document_type Some(DocumentType::Regular),
1087 referrer_url Some("https://news.website/tech"),
1088 title None
1089 );
1090
1091 assert_total_after_observation!(&conn,
1093 total_records_after 5,
1094 total_view_time_after 100000,
1095 url "https://www.youtube.com/watch?v=tpiyEe_CqB4",
1096 view_time Some(100000),
1097 search_term Some("cute cat"),
1098 document_type Some(DocumentType::Media),
1099 referrer_url Some("https://www.youtube.com/results?search_query=cute+cat"),
1100 title None
1101 );
1102
1103 assert_total_after_observation!(&conn,
1105 total_records_after 6,
1106 total_view_time_after 80000,
1107 url "https://www.youtube.com/watch?v=daff43jif3",
1108 view_time Some(80000),
1109 search_term Some(""),
1110 document_type Some(DocumentType::Media),
1111 referrer_url Some(""),
1112 title None
1113 );
1114
1115 assert_total_after_observation!(&conn,
1116 total_records_after 6,
1117 total_view_time_after 90000,
1118 url "https://www.youtube.com/watch?v=daff43jif3",
1119 view_time Some(10000),
1120 search_term None,
1121 document_type Some(DocumentType::Media),
1122 referrer_url None,
1123 title None
1124 );
1125
1126 assert_total_after_observation!(&conn,
1128 total_records_after 7,
1129 total_view_time_after 0,
1130 url "https://www.youtube.com/watch?v=fds32fds",
1131 view_time None,
1132 search_term None,
1133 document_type Some(DocumentType::Media),
1134 referrer_url None,
1135 title None
1136 );
1137
1138 assert_total_after_observation!(&conn,
1140 total_records_after 7,
1141 total_view_time_after 1338,
1142 url "https://www.youtube.com/watch?v=fds32fds",
1143 view_time Some(1338),
1144 search_term None,
1145 document_type None,
1146 referrer_url None,
1147 title None
1148 );
1149
1150 assert_total_after_observation!(&conn,
1152 total_records_after 7,
1153 total_view_time_after 2000,
1154 url "https://www.youtube.com/watch?v=fds32fds",
1155 view_time Some(662),
1156 search_term None,
1157 document_type None,
1158 referrer_url None,
1159 title None
1160 );
1161
1162 assert_after_observation!(&conn,
1165 total_records_after 8,
1166 total_view_time_after 662,
1167 url "https://www.youtube.com/watch?v=dasdg34d",
1168 view_time Some(662),
1169 search_term None,
1170 document_type None,
1171 referrer_url None,
1172 title None,
1173 assertion |m: HistoryMetadata| { assert_eq!(DocumentType::Regular, m.document_type) }
1174 );
1175
1176 assert_after_observation!(&conn,
1177 total_records_after 8,
1178 total_view_time_after 662,
1179 url "https://www.youtube.com/watch?v=dasdg34d",
1180 view_time None,
1181 search_term None,
1182 document_type Some(DocumentType::Media),
1183 referrer_url None,
1184 title None,
1185 assertion |m: HistoryMetadata| { assert_eq!(DocumentType::Media, m.document_type) }
1186 );
1187
1188 assert_after_observation!(&conn,
1190 total_records_after 8,
1191 total_view_time_after 675,
1192 url "https://www.youtube.com/watch?v=dasdg34d",
1193 view_time Some(13),
1194 search_term None,
1195 document_type None,
1196 referrer_url None,
1197 title None,
1198 assertion |m: HistoryMetadata| { assert_eq!(DocumentType::Media, m.document_type) }
1199 );
1200
1201 assert_after_observation!(&conn,
1203 total_records_after 9,
1204 total_view_time_after 13,
1205 url "https://www.youtube.com/watch?v=dasdsada",
1206 view_time Some(13),
1207 search_term None,
1208 document_type None,
1209 referrer_url None,
1210 title Some("hello!"),
1211 assertion |m: HistoryMetadata| { assert_eq!(Some(String::from("hello!")), m.title) }
1212 );
1213
1214 assert_after_observation!(&conn,
1216 total_records_after 9,
1217 total_view_time_after 26,
1218 url "https://www.youtube.com/watch?v=dasdsada",
1219 view_time Some(13),
1220 search_term None,
1221 document_type None,
1222 referrer_url None,
1223 title Some("world!"),
1224 assertion |m: HistoryMetadata| { assert_eq!(Some(String::from("hello!")), m.title) }
1225 );
1226 }
1227
1228 #[test]
1229 fn test_note_observation_invalid_view_time() {
1230 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1231
1232 note_observation!(&conn,
1233 url "https://www.mozilla.org/",
1234 view_time None,
1235 search_term None,
1236 document_type Some(DocumentType::Regular),
1237 referrer_url None,
1238 title None
1239 );
1240
1241 assert!(apply_metadata_observation(
1243 &conn,
1244 HistoryMetadataObservation {
1245 url: String::from("https://www.mozilla.org"),
1246 view_time: Some(1000 * 60 * 60 * 24 * 2),
1247 search_term: None,
1248 document_type: None,
1249 referrer_url: None,
1250 title: None
1251 },
1252 NoteHistoryMetadataObservationOptions::new(),
1253 )
1254 .is_err());
1255
1256 assert!(apply_metadata_observation(
1258 &conn,
1259 HistoryMetadataObservation {
1260 url: String::from("https://www.mozilla.org"),
1261 view_time: Some(1000 * 60 * 60 * 12),
1262 search_term: None,
1263 document_type: None,
1264 referrer_url: None,
1265 title: None
1266 },
1267 NoteHistoryMetadataObservationOptions::new(),
1268 )
1269 .is_ok());
1270 }
1271
1272 #[test]
1273 fn test_get_between() {
1274 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1275
1276 assert_eq!(0, get_between(&conn, 0, 0).unwrap().len());
1277
1278 let beginning = Timestamp::now().as_millis() as i64;
1279 note_observation!(&conn,
1280 url "http://mozilla.com/another",
1281 view_time Some(3000),
1282 search_term None,
1283 document_type Some(DocumentType::Regular),
1284 referrer_url Some("https://news.website/tech"),
1285 title None
1286 );
1287 let after_meta1 = Timestamp::now().as_millis() as i64;
1288
1289 assert_eq!(0, get_between(&conn, 0, beginning - 1).unwrap().len());
1290 assert_eq!(1, get_between(&conn, 0, after_meta1).unwrap().len());
1291
1292 bump_clock();
1293
1294 note_observation!(&conn,
1295 url "http://mozilla.com/video/",
1296 view_time Some(1000),
1297 search_term None,
1298 document_type Some(DocumentType::Media),
1299 referrer_url None,
1300 title None
1301 );
1302 let after_meta2 = Timestamp::now().as_millis() as i64;
1303
1304 assert_eq!(1, get_between(&conn, beginning, after_meta1).unwrap().len());
1305 assert_eq!(2, get_between(&conn, beginning, after_meta2).unwrap().len());
1306 assert_eq!(
1307 1,
1308 get_between(&conn, after_meta1, after_meta2).unwrap().len()
1309 );
1310 assert_eq!(
1311 0,
1312 get_between(&conn, after_meta2, after_meta2 + 1)
1313 .unwrap()
1314 .len()
1315 );
1316 }
1317
1318 #[test]
1319 fn test_get_since() {
1320 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1321
1322 assert_eq!(0, get_since(&conn, 0).unwrap().len());
1323
1324 let beginning = Timestamp::now().as_millis() as i64;
1325 note_observation!(&conn,
1326 url "http://mozilla.com/another",
1327 view_time Some(3000),
1328 search_term None,
1329 document_type Some(DocumentType::Regular),
1330 referrer_url Some("https://news.website/tech"),
1331 title None
1332 );
1333 let after_meta1 = Timestamp::now().as_millis() as i64;
1334
1335 assert_eq!(1, get_since(&conn, 0).unwrap().len());
1336 assert_eq!(1, get_since(&conn, beginning).unwrap().len());
1337 assert_eq!(0, get_since(&conn, after_meta1).unwrap().len());
1338
1339 note_observation!(&conn,
1342 url "http://mozilla.com/video/",
1343 view_time Some(1000),
1344 search_term None,
1345 document_type Some(DocumentType::Media),
1346 referrer_url None,
1347 title None
1348 );
1349 let after_meta2 = Timestamp::now().as_millis() as i64;
1350 assert_eq!(2, get_since(&conn, beginning).unwrap().len());
1351 assert_eq!(1, get_since(&conn, after_meta1).unwrap().len());
1352 assert_eq!(0, get_since(&conn, after_meta2).unwrap().len());
1353 }
1354
1355 #[test]
1356 fn test_get_most_recent_empty() {
1357 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1358 let rows = get_most_recent(&conn, 5).expect("query ok");
1359 assert!(rows.is_empty());
1360 }
1361
1362 #[test]
1363 fn test_get_most_recent_orders_and_limits_same_observation() {
1364 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1365
1366 note_observation!(&conn,
1367 url "https://example.com/1",
1368 view_time Some(10),
1369 search_term None,
1370 document_type Some(DocumentType::Regular),
1371 referrer_url None,
1372 title None
1373 );
1374
1375 bump_clock();
1376
1377 note_observation!(&conn,
1378 url "https://example.com/1",
1379 view_time Some(10),
1380 search_term None,
1381 document_type Some(DocumentType::Regular),
1382 referrer_url None,
1383 title None
1384 );
1385
1386 bump_clock();
1387
1388 note_observation!(&conn,
1389 url "https://example.com/1",
1390 view_time Some(10),
1391 search_term None,
1392 document_type Some(DocumentType::Regular),
1393 referrer_url None,
1394 title None
1395 );
1396
1397 let most_recents1 = get_most_recent(&conn, 1).expect("query ok");
1399 assert_eq!(most_recents1.len(), 1);
1400 assert_eq!(most_recents1[0].url, "https://example.com/1");
1401
1402 let most_recents2 = get_most_recent(&conn, 3).expect("query ok");
1404 assert_eq!(most_recents2.len(), 1);
1405 assert_eq!(most_recents2[0].url, "https://example.com/1");
1406
1407 let most_recents3 = get_most_recent(&conn, 10).expect("query ok");
1409 assert_eq!(most_recents3.len(), 1);
1410 assert_eq!(most_recents3[0].url, "https://example.com/1");
1411 }
1412
1413 #[test]
1414 fn test_get_most_recent_orders_and_limits_different_observations() {
1415 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1416
1417 note_observation!(&conn,
1418 url "https://example.com/1",
1419 view_time Some(10),
1420 search_term None,
1421 document_type Some(DocumentType::Regular),
1422 referrer_url None,
1423 title None
1424 );
1425
1426 bump_clock();
1427
1428 note_observation!(&conn,
1429 url "https://example.com/2",
1430 view_time Some(20),
1431 search_term None,
1432 document_type Some(DocumentType::Regular),
1433 referrer_url None,
1434 title None
1435 );
1436
1437 bump_clock();
1438
1439 note_observation!(&conn,
1440 url "https://example.com/3",
1441 view_time Some(30),
1442 search_term None,
1443 document_type Some(DocumentType::Regular),
1444 referrer_url None,
1445 title None
1446 );
1447
1448 let most_recents1 = get_most_recent(&conn, 1).expect("query ok");
1450 assert_eq!(most_recents1.len(), 1);
1451 assert_eq!(most_recents1[0].url, "https://example.com/3");
1452
1453 let most_recents2 = get_most_recent(&conn, 2).expect("query ok");
1455 assert_eq!(most_recents2.len(), 2);
1456 assert_eq!(most_recents2[0].url, "https://example.com/3");
1457 assert_eq!(most_recents2[1].url, "https://example.com/2");
1458
1459 let most_recents3 = get_most_recent(&conn, 10).expect("query ok");
1461 assert_eq!(most_recents3.len(), 3);
1462 assert_eq!(most_recents3[0].url, "https://example.com/3");
1463 assert_eq!(most_recents3[1].url, "https://example.com/2");
1464 assert_eq!(most_recents3[2].url, "https://example.com/1");
1465 }
1466
1467 #[test]
1468 fn test_get_most_recent_negative_limit() {
1469 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1470
1471 note_observation!(&conn,
1472 url "https://example.com/1",
1473 view_time Some(10),
1474 search_term None,
1475 document_type Some(DocumentType::Regular),
1476 referrer_url None,
1477 title None
1478 );
1479
1480 bump_clock();
1481
1482 note_observation!(&conn,
1483 url "https://example.com/2",
1484 view_time Some(10),
1485 search_term None,
1486 document_type Some(DocumentType::Regular),
1487 referrer_url None,
1488 title None
1489 );
1490
1491 bump_clock();
1492
1493 note_observation!(&conn,
1494 url "https://example.com/3",
1495 view_time Some(10),
1496 search_term None,
1497 document_type Some(DocumentType::Regular),
1498 referrer_url None,
1499 title None
1500 );
1501
1502 let most_recents = get_most_recent(&conn, -1).expect("query ok");
1504 assert_eq!(most_recents.len(), 3);
1505 assert_eq!(most_recents[0].url, "https://example.com/3");
1506 assert_eq!(most_recents[1].url, "https://example.com/2");
1507 assert_eq!(most_recents[2].url, "https://example.com/1");
1508 }
1509
1510 #[test]
1511 fn test_get_most_recent_search_entries_empty() {
1512 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1513 let rows = get_most_recent_search_entries(&conn, 5).expect("query ok");
1514 assert!(rows.is_empty());
1515 }
1516
1517 #[test]
1518 fn test_get_most_recent_search_entries_with_limits_and_same_observation() {
1519 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1520
1521 note_observation!(&conn,
1522 url "http://mozilla.org/1/",
1523 view_time None,
1524 search_term Some("search_term_1"),
1525 document_type None,
1526 referrer_url None,
1527 title None
1528 );
1529
1530 bump_clock();
1531
1532 note_observation!(&conn,
1533 url "http://mozilla.org/1/",
1534 view_time None,
1535 search_term Some("search_term_1"),
1536 document_type None,
1537 referrer_url None,
1538 title None
1539 );
1540
1541 bump_clock();
1542
1543 note_observation!(&conn,
1544 url "http://mozilla.org/1/",
1545 view_time None,
1546 search_term Some("search_term_1"),
1547 document_type None,
1548 referrer_url None,
1549 title None
1550 );
1551
1552 let most_recents1 = get_most_recent_search_entries(&conn, 1).expect("query ok");
1554 assert_eq!(most_recents1.len(), 1);
1555 assert_eq!(most_recents1[0].url, "http://mozilla.org/1/");
1556
1557 let most_recents2 = get_most_recent_search_entries(&conn, 3).expect("query ok");
1559 assert_eq!(most_recents2.len(), 1);
1560 assert_eq!(most_recents2[0].url, "http://mozilla.org/1/");
1561
1562 let most_recents3 = get_most_recent_search_entries(&conn, 10).expect("query ok");
1564 assert_eq!(most_recents3.len(), 1);
1565 assert_eq!(most_recents3[0].url, "http://mozilla.org/1/");
1566 }
1567
1568 #[test]
1569 fn test_get_most_recent_search_entries_with_limits_and_different_observations() {
1570 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1571
1572 note_observation!(&conn,
1573 url "http://mozilla.org/1/",
1574 view_time None,
1575 search_term Some("search_term_1"),
1576 document_type None,
1577 referrer_url None,
1578 title None
1579 );
1580
1581 bump_clock();
1582
1583 note_observation!(&conn,
1584 url "http://mozilla.org/2/",
1585 view_time Some(20),
1586 search_term None,
1587 document_type Some(DocumentType::Regular),
1588 referrer_url None,
1589 title None
1590 );
1591
1592 bump_clock();
1593
1594 note_observation!(&conn,
1595 url "http://mozilla.org/3/",
1596 view_time None,
1597 search_term Some("search_term_2"),
1598 document_type None,
1599 referrer_url None,
1600 title None
1601 );
1602
1603 bump_clock();
1604
1605 note_observation!(&conn,
1606 url "http://mozilla.org/4/",
1607 view_time None,
1608 search_term Some("search_term_3"),
1609 document_type None,
1610 referrer_url None,
1611 title None
1612 );
1613
1614 let most_recents1 = get_most_recent_search_entries(&conn, 1).expect("query ok");
1616 assert_eq!(most_recents1.len(), 1);
1617 assert_eq!(most_recents1[0].url, "http://mozilla.org/4/");
1618
1619 let most_recents2 = get_most_recent_search_entries(&conn, 2).expect("query ok");
1621 assert_eq!(most_recents2.len(), 2);
1622 assert_eq!(most_recents2[0].url, "http://mozilla.org/4/");
1623 assert_eq!(most_recents2[1].url, "http://mozilla.org/3/");
1624
1625 let most_recents3 = get_most_recent_search_entries(&conn, 10).expect("query ok");
1627 assert_eq!(most_recents3.len(), 3);
1628 assert_eq!(most_recents3[0].url, "http://mozilla.org/4/");
1629 assert_eq!(most_recents3[1].url, "http://mozilla.org/3/");
1630 assert_eq!(most_recents3[2].url, "http://mozilla.org/1/");
1631 }
1632
1633 #[test]
1634 fn test_get_most_recent_search_entries_with_negative_limit_with_same_observation() {
1635 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1636
1637 note_observation!(&conn,
1638 url "http://mozilla.org/1/",
1639 view_time None,
1640 search_term Some("search_term_1"),
1641 document_type None,
1642 referrer_url None,
1643 title None
1644 );
1645
1646 bump_clock();
1647
1648 note_observation!(&conn,
1649 url "http://mozilla.org/1/",
1650 view_time None,
1651 search_term Some("search_term_1"),
1652 document_type None,
1653 referrer_url None,
1654 title None
1655 );
1656
1657 bump_clock();
1658
1659 note_observation!(&conn,
1660 url "http://mozilla.org/1/",
1661 view_time None,
1662 search_term Some("search_term_1"),
1663 document_type None,
1664 referrer_url None,
1665 title None
1666 );
1667
1668 let most_recents = get_most_recent_search_entries(&conn, -1).expect("query ok");
1670 assert_eq!(most_recents.len(), 1);
1671 assert_eq!(most_recents[0].url, "http://mozilla.org/1/");
1672 }
1673
1674 #[test]
1675 fn test_get_most_recent_search_entries_with_negative_limit_with_different_observations() {
1676 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1677
1678 note_observation!(&conn,
1679 url "http://mozilla.org/1/",
1680 view_time None,
1681 search_term Some("search_term_1"),
1682 document_type None,
1683 referrer_url None,
1684 title None
1685 );
1686
1687 bump_clock();
1688
1689 note_observation!(&conn,
1690 url "http://mozilla.org/2/",
1691 view_time None,
1692 search_term Some("search_term_2"),
1693 document_type None,
1694 referrer_url None,
1695 title None
1696 );
1697
1698 bump_clock();
1699
1700 note_observation!(&conn,
1701 url "http://mozilla.org/3/",
1702 view_time None,
1703 search_term Some("search_term_3"),
1704 document_type None,
1705 referrer_url None,
1706 title None
1707 );
1708
1709 let most_recents = get_most_recent_search_entries(&conn, -1).expect("query ok");
1711 assert_eq!(most_recents.len(), 3);
1712 assert_eq!(most_recents[0].url, "http://mozilla.org/3/");
1713 assert_eq!(most_recents[1].url, "http://mozilla.org/2/");
1714 assert_eq!(most_recents[2].url, "http://mozilla.org/1/");
1715 }
1716
1717 #[test]
1718 fn test_get_highlights() {
1719 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1720
1721 assert_eq!(
1723 0,
1724 get_highlights(
1725 &conn,
1726 HistoryHighlightWeights {
1727 view_time: 1.0,
1728 frequency: 1.0
1729 },
1730 10
1731 )
1732 .unwrap()
1733 .len()
1734 );
1735
1736 apply_observation(
1738 &conn,
1739 VisitObservation::new(
1740 Url::parse("https://www.reddit.com/r/climbing").expect("Should parse URL"),
1741 )
1742 .with_visit_type(VisitType::Link)
1743 .with_at(Timestamp::now()),
1744 )
1745 .expect("Should apply observation");
1746 assert_eq!(
1747 0,
1748 get_highlights(
1749 &conn,
1750 HistoryHighlightWeights {
1751 view_time: 1.0,
1752 frequency: 1.0
1753 },
1754 10
1755 )
1756 .unwrap()
1757 .len()
1758 );
1759
1760 note_observation!(&conn,
1762 url "http://mozilla.com/1",
1763 view_time Some(1000),
1764 search_term None,
1765 document_type Some(DocumentType::Regular),
1766 referrer_url Some("https://news.website/tech"),
1767 title None
1768 );
1769
1770 note_observation!(&conn,
1771 url "http://mozilla.com/1",
1772 view_time Some(1000),
1773 search_term None,
1774 document_type Some(DocumentType::Regular),
1775 referrer_url Some("https://news.website/tech"),
1776 title None
1777 );
1778
1779 note_observation!(&conn,
1780 url "http://mozilla.com/1",
1781 view_time Some(1000),
1782 search_term None,
1783 document_type Some(DocumentType::Regular),
1784 referrer_url Some("https://news.website/tech"),
1785 title None
1786 );
1787
1788 note_observation!(&conn,
1790 url "http://mozilla.com/2",
1791 view_time Some(3500),
1792 search_term None,
1793 document_type Some(DocumentType::Regular),
1794 referrer_url Some("https://news.website/tech"),
1795 title None
1796 );
1797
1798 let even_weights = HistoryHighlightWeights {
1805 view_time: 1.0,
1806 frequency: 1.0,
1807 };
1808 let highlights1 = get_highlights(&conn, even_weights.clone(), 10).unwrap();
1809 assert_eq!(2, highlights1.len());
1810 assert_eq!("http://mozilla.com/2", highlights1[0].url);
1811
1812 let frequency_heavy_weights = HistoryHighlightWeights {
1814 view_time: 1.0,
1815 frequency: 100.0,
1816 };
1817 let highlights2 = get_highlights(&conn, frequency_heavy_weights, 10).unwrap();
1818 assert_eq!(2, highlights2.len());
1819 assert_eq!("http://mozilla.com/2", highlights2[0].url);
1820
1821 note_observation!(&conn,
1825 url "http://mozilla.com/1",
1826 view_time Some(100),
1827 search_term Some("test search"),
1828 document_type Some(DocumentType::Regular),
1829 referrer_url Some("https://news.website/tech"),
1830 title None
1831 );
1832
1833 let highlights3 = get_highlights(&conn, even_weights, 10).unwrap();
1835 assert_eq!(2, highlights3.len());
1836 assert_eq!("http://mozilla.com/1", highlights3[0].url);
1837
1838 let view_time_heavy_weights = HistoryHighlightWeights {
1841 view_time: 6.0,
1842 frequency: 1.0,
1843 };
1844 let highlights4 = get_highlights(&conn, view_time_heavy_weights, 10).unwrap();
1845 assert_eq!(2, highlights4.len());
1846 assert_eq!("http://mozilla.com/2", highlights4[0].url);
1847 }
1848
1849 #[test]
1850 fn test_get_highlights_no_viewtime() {
1851 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1852
1853 note_observation!(&conn,
1855 url "http://mozilla.com/1",
1856 view_time Some(0),
1857 search_term None,
1858 document_type Some(DocumentType::Regular),
1859 referrer_url Some("https://news.website/tech"),
1860 title None
1861 );
1862 let highlights = get_highlights(
1863 &conn,
1864 HistoryHighlightWeights {
1865 view_time: 1.0,
1866 frequency: 1.0,
1867 },
1868 2,
1869 )
1870 .unwrap();
1871 assert_eq!(highlights.len(), 1);
1872 assert_eq!(highlights[0].score, 0.0);
1873 }
1874
1875 #[test]
1876 fn test_query() {
1877 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1878 let now = Timestamp::now();
1879
1880 let observation1 = VisitObservation::new(Url::parse("https://www.cbc.ca/news/politics/federal-budget-2021-freeland-zimonjic-1.5991021").unwrap())
1882 .with_at(now)
1883 .with_title(Some(String::from("Budget vows to build 'for the long term' as it promises child care cash, projects massive deficits | CBC News")))
1884 .with_preview_image_url(Some(Url::parse("https://i.cbc.ca/1.5993583.1618861792!/cpImage/httpImage/image.jpg_gen/derivatives/16x9_620/fedbudget-20210419.jpg").unwrap()))
1885 .with_is_remote(false)
1886 .with_visit_type(VisitType::Link);
1887 apply_observation(&conn, observation1).unwrap();
1888
1889 note_observation!(
1890 &conn,
1891 url "https://www.cbc.ca/news/politics/federal-budget-2021-freeland-zimonjic-1.5991021",
1892 view_time Some(20000),
1893 search_term Some("cbc federal budget 2021"),
1894 document_type Some(DocumentType::Regular),
1895 referrer_url Some("https://yandex.ru/search/?text=cbc%20federal%20budget%202021&lr=21512"),
1896 title None
1897 );
1898
1899 note_observation!(
1900 &conn,
1901 url "https://stackoverflow.com/questions/37777675/how-to-create-a-formatted-string-out-of-a-literal-in-rust",
1902 view_time Some(20000),
1903 search_term Some("rust string format"),
1904 document_type Some(DocumentType::Regular),
1905 referrer_url Some("https://yandex.ru/search/?lr=21512&text=rust%20string%20format"),
1906 title None
1907 );
1908
1909 note_observation!(
1910 &conn,
1911 url "https://www.sqlite.org/lang_corefunc.html#instr",
1912 view_time Some(20000),
1913 search_term Some("sqlite like"),
1914 document_type Some(DocumentType::Regular),
1915 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=sqlite+like"),
1916 title None
1917 );
1918
1919 note_observation!(
1920 &conn,
1921 url "https://www.youtube.com/watch?v=tpiyEe_CqB4",
1922 view_time Some(100000),
1923 search_term Some("cute cat"),
1924 document_type Some(DocumentType::Media),
1925 referrer_url Some("https://www.youtube.com/results?search_query=cute+cat"),
1926 title None
1927 );
1928
1929 let meta = query(&conn, "child care", 10).expect("query should work");
1931 assert_eq!(1, meta.len(), "expected exactly one result");
1932 assert_history_metadata_record!(meta[0],
1933 url "https://www.cbc.ca/news/politics/federal-budget-2021-freeland-zimonjic-1.5991021",
1934 total_time 20000,
1935 search_term Some("cbc federal budget 2021"),
1936 document_type DocumentType::Regular,
1937 referrer_url Some("https://yandex.ru/search/?text=cbc%20federal%20budget%202021&lr=21512"),
1938 title Some("Budget vows to build 'for the long term' as it promises child care cash, projects massive deficits | CBC News"),
1939 preview_image_url Some("https://i.cbc.ca/1.5993583.1618861792!/cpImage/httpImage/image.jpg_gen/derivatives/16x9_620/fedbudget-20210419.jpg")
1940 );
1941
1942 let meta = query(&conn, "string format", 10).expect("query should work");
1944 assert_eq!(1, meta.len(), "expected exactly one result");
1945 assert_history_metadata_record!(meta[0],
1946 url "https://stackoverflow.com/questions/37777675/how-to-create-a-formatted-string-out-of-a-literal-in-rust",
1947 total_time 20000,
1948 search_term Some("rust string format"),
1949 document_type DocumentType::Regular,
1950 referrer_url Some("https://yandex.ru/search/?lr=21512&text=rust%20string%20format"),
1951 title None,
1952 preview_image_url None
1953 );
1954
1955 let meta = query(&conn, "instr", 10).expect("query should work");
1957 assert_history_metadata_record!(meta[0],
1958 url "https://www.sqlite.org/lang_corefunc.html#instr",
1959 total_time 20000,
1960 search_term Some("sqlite like"),
1961 document_type DocumentType::Regular,
1962 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=sqlite+like"),
1963 title None,
1964 preview_image_url None
1965 );
1966
1967 let meta = query(&conn, "youtube", 10).expect("query should work");
1969 assert_history_metadata_record!(meta[0],
1970 url "https://www.youtube.com/watch?v=tpiyEe_CqB4",
1971 total_time 100000,
1972 search_term Some("cute cat"),
1973 document_type DocumentType::Media,
1974 referrer_url Some("https://www.youtube.com/results?search_query=cute+cat"),
1975 title None,
1976 preview_image_url None
1977 );
1978 }
1979
1980 #[test]
1981 fn test_delete_metadata() {
1982 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1983
1984 note_observation!(&conn,
1991 url "http://mozilla.com/1",
1992 view_time Some(20000),
1993 search_term Some("1 with search"),
1994 document_type Some(DocumentType::Regular),
1995 referrer_url Some("http://mozilla.com/"),
1996 title None
1997 );
1998
1999 note_observation!(&conn,
2000 url "http://mozilla.com/1",
2001 view_time Some(20000),
2002 search_term Some("1 with search"),
2003 document_type Some(DocumentType::Regular),
2004 referrer_url None,
2005 title None
2006 );
2007
2008 note_observation!(&conn,
2009 url "http://mozilla.com/1",
2010 view_time Some(20000),
2011 search_term None,
2012 document_type Some(DocumentType::Regular),
2013 referrer_url Some("http://mozilla.com/"),
2014 title None
2015 );
2016
2017 note_observation!(&conn,
2018 url "http://mozilla.com/1",
2019 view_time Some(20000),
2020 search_term None,
2021 document_type Some(DocumentType::Regular),
2022 referrer_url None,
2023 title None
2024 );
2025
2026 note_observation!(&conn,
2027 url "http://mozilla.com/2",
2028 view_time Some(20000),
2029 search_term None,
2030 document_type Some(DocumentType::Regular),
2031 referrer_url None,
2032 title None
2033 );
2034
2035 note_observation!(&conn,
2036 url "http://mozilla.com/2",
2037 view_time Some(20000),
2038 search_term None,
2039 document_type Some(DocumentType::Regular),
2040 referrer_url Some("http://mozilla.com/"),
2041 title None
2042 );
2043
2044 bump_clock();
2045 note_observation!(&conn,
2047 url "http://mozilla.com/2",
2048 view_time Some(20000),
2049 search_term None,
2050 document_type Some(DocumentType::Regular),
2051 referrer_url Some("http://mozilla.com/"),
2052 title None
2053 );
2054
2055 assert_eq!(6, get_since(&conn, 0).expect("get worked").len());
2056 delete_metadata(
2057 &conn,
2058 &Url::parse("http://mozilla.com/1").unwrap(),
2059 None,
2060 None,
2061 )
2062 .expect("delete metadata");
2063 assert_eq!(5, get_since(&conn, 0).expect("get worked").len());
2064
2065 delete_metadata(
2066 &conn,
2067 &Url::parse("http://mozilla.com/1").unwrap(),
2068 Some(&Url::parse("http://mozilla.com/").unwrap()),
2069 None,
2070 )
2071 .expect("delete metadata");
2072 assert_eq!(4, get_since(&conn, 0).expect("get worked").len());
2073
2074 delete_metadata(
2075 &conn,
2076 &Url::parse("http://mozilla.com/1").unwrap(),
2077 Some(&Url::parse("http://mozilla.com/").unwrap()),
2078 Some("1 with search"),
2079 )
2080 .expect("delete metadata");
2081 assert_eq!(3, get_since(&conn, 0).expect("get worked").len());
2082
2083 delete_metadata(
2084 &conn,
2085 &Url::parse("http://mozilla.com/1").unwrap(),
2086 None,
2087 Some("1 with search"),
2088 )
2089 .expect("delete metadata");
2090 assert_eq!(2, get_since(&conn, 0).expect("get worked").len());
2091
2092 delete_metadata(
2094 &conn,
2095 &Url::parse("http://mozilla.com/2").unwrap(),
2096 Some(&Url::parse("http://wrong-referrer.com").unwrap()),
2097 Some("2 with search"),
2098 )
2099 .expect("delete metadata");
2100 assert_eq!(2, get_since(&conn, 0).expect("get worked").len());
2101 }
2102
2103 #[test]
2104 fn test_delete_older_than() {
2105 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2106
2107 let beginning = Timestamp::now().as_millis() as i64;
2108
2109 note_observation!(&conn,
2110 url "http://mozilla.com/1",
2111 view_time Some(20000),
2112 search_term None,
2113 document_type Some(DocumentType::Regular),
2114 referrer_url None,
2115 title None
2116 );
2117 let after_meta1 = Timestamp::now().as_millis() as i64;
2118
2119 bump_clock();
2120
2121 note_observation!(&conn,
2122 url "http://mozilla.com/2",
2123 view_time Some(20000),
2124 search_term None,
2125 document_type Some(DocumentType::Regular),
2126 referrer_url None,
2127 title None
2128 );
2129
2130 bump_clock();
2131
2132 note_observation!(&conn,
2133 url "http://mozilla.com/3",
2134 view_time Some(20000),
2135 search_term None,
2136 document_type Some(DocumentType::Regular),
2137 referrer_url None,
2138 title None
2139 );
2140 let after_meta3 = Timestamp::now().as_millis() as i64;
2141
2142 delete_older_than(&conn, beginning).expect("delete worked");
2144 assert_eq!(3, get_since(&conn, beginning).expect("get worked").len());
2145
2146 delete_older_than(&conn, after_meta1).expect("delete worked");
2148 assert_eq!(2, get_since(&conn, beginning).expect("get worked").len());
2149 assert_eq!(
2150 None,
2151 get_latest_for_url(&conn, &Url::parse("http://mozilla.com/1").expect("url"))
2152 .expect("get")
2153 );
2154
2155 delete_older_than(&conn, after_meta3).expect("delete worked");
2157 assert_eq!(0, get_since(&conn, beginning).expect("get worked").len());
2158 }
2159
2160 #[test]
2161 fn test_delete_between() {
2162 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2163
2164 let beginning = Timestamp::now().as_millis() as i64;
2165 bump_clock();
2166
2167 note_observation!(&conn,
2168 url "http://mozilla.com/1",
2169 view_time Some(20000),
2170 search_term None,
2171 document_type Some(DocumentType::Regular),
2172 referrer_url None,
2173 title None
2174 );
2175
2176 bump_clock();
2177
2178 note_observation!(&conn,
2179 url "http://mozilla.com/2",
2180 view_time Some(20000),
2181 search_term None,
2182 document_type Some(DocumentType::Regular),
2183 referrer_url None,
2184 title None
2185 );
2186 let after_meta2 = Timestamp::now().as_millis() as i64;
2187
2188 bump_clock();
2189
2190 note_observation!(&conn,
2191 url "http://mozilla.com/3",
2192 view_time Some(20000),
2193 search_term None,
2194 document_type Some(DocumentType::Regular),
2195 referrer_url None,
2196 title None
2197 );
2198 let after_meta3 = Timestamp::now().as_millis() as i64;
2199
2200 delete_between(&conn, after_meta2, after_meta3).expect("delete worked");
2202 assert_eq!(2, get_since(&conn, beginning).expect("get worked").len());
2203 assert_eq!(
2204 None,
2205 get_latest_for_url(&conn, &Url::parse("http://mozilla.com/3").expect("url"))
2206 .expect("get")
2207 );
2208 }
2209
2210 #[test]
2211 fn test_metadata_deletes_do_not_affect_places() {
2212 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2213
2214 note_observation!(
2215 &conn,
2216 url "https://www.mozilla.org/first/",
2217 view_time Some(20000),
2218 search_term None,
2219 document_type Some(DocumentType::Regular),
2220 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2221 title None
2222 );
2223
2224 note_observation!(
2225 &conn,
2226 url "https://www.mozilla.org/",
2227 view_time Some(20000),
2228 search_term None,
2229 document_type Some(DocumentType::Regular),
2230 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2231 title None
2232 );
2233 let after_meta_added = Timestamp::now().as_millis() as i64;
2234
2235 delete_older_than(&conn, after_meta_added).expect("delete older than worked");
2237
2238 assert_table_size!(&conn, "moz_places", 3);
2241 }
2242
2243 #[test]
2244 fn test_delete_history_also_deletes_metadata_bookmarked() {
2245 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2246 let url = Url::parse("https://www.mozilla.org/bookmarked").unwrap();
2248 let bm_guid: SyncGuid = "bookmarkAAAA".into();
2249 let bm = InsertableBookmark {
2250 parent_guid: BookmarkRootGuid::Unfiled.into(),
2251 position: BookmarkPosition::Append,
2252 date_added: None,
2253 last_modified: None,
2254 guid: Some(bm_guid.clone()),
2255 url: url.clone(),
2256 title: Some("bookmarked page".to_string()),
2257 };
2258 insert_bookmark(&conn, InsertableItem::Bookmark { b: bm }).expect("bookmark should insert");
2259 let obs = VisitObservation::new(url.clone()).with_visit_type(VisitType::Link);
2260 apply_observation(&conn, obs).expect("Should apply visit");
2261 note_observation!(
2262 &conn,
2263 url url.to_string(),
2264 view_time Some(20000),
2265 search_term None,
2266 document_type Some(DocumentType::Regular),
2267 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2268 title None
2269 );
2270
2271 assert_eq!(
2273 get_visit_count(&conn, VisitTransitionSet::empty()).unwrap(),
2274 1
2275 );
2276 let place_guid = url_to_guid(&conn, &url)
2277 .expect("is valid")
2278 .expect("should exist");
2279
2280 delete_visits_for(&conn, &place_guid).expect("should work");
2281 assert!(get_raw_bookmark(&conn, &bm_guid).unwrap().is_some());
2283 let pi = fetch_page_info(&conn, &url)
2285 .expect("should work")
2286 .expect("should exist");
2287 assert!(pi.last_visit_id.is_none());
2288 assert!(get_latest_for_url(&conn, &url)
2290 .expect("should work")
2291 .is_none());
2292 }
2293
2294 #[test]
2295 fn test_delete_history_also_deletes_metadata_not_bookmarked() {
2296 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2297 let url = Url::parse("https://www.mozilla.org/not-bookmarked").unwrap();
2299 let obs = VisitObservation::new(url.clone()).with_visit_type(VisitType::Link);
2300 apply_observation(&conn, obs).expect("Should apply visit");
2301 note_observation!(
2302 &conn,
2303 url url.to_string(),
2304 view_time Some(20000),
2305 search_term None,
2306 document_type Some(DocumentType::Regular),
2307 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2308 title None
2309 );
2310
2311 assert_eq!(
2313 get_visit_count(&conn, VisitTransitionSet::empty()).unwrap(),
2314 1
2315 );
2316 let place_guid = url_to_guid(&conn, &url)
2317 .expect("is valid")
2318 .expect("should exist");
2319
2320 delete_visits_for(&conn, &place_guid).expect("should work");
2321 assert!(fetch_page_info(&conn, &url).expect("should work").is_none());
2323 assert!(get_latest_for_url(&conn, &url)
2324 .expect("should work")
2325 .is_none());
2326 }
2327
2328 #[test]
2329 fn test_delete_history_also_deletes_metadata_no_visits() {
2330 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2331 let url = Url::parse("https://www.mozilla.org/no-visits").unwrap();
2333 note_observation!(
2334 &conn,
2335 url url.to_string(),
2336 view_time Some(20000),
2337 search_term None,
2338 document_type Some(DocumentType::Regular),
2339 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2340 title None
2341 );
2342
2343 assert_eq!(
2345 get_visit_count(&conn, VisitTransitionSet::empty()).unwrap(),
2346 0
2347 );
2348 let place_guid = url_to_guid(&conn, &url)
2349 .expect("is valid")
2350 .expect("should exist");
2351
2352 delete_visits_for(&conn, &place_guid).expect("should work");
2353 assert!(fetch_page_info(&conn, &url).expect("should work").is_none());
2355 assert!(get_latest_for_url(&conn, &url)
2356 .expect("should work")
2357 .is_none());
2358 }
2359
2360 #[test]
2361 fn test_delete_between_also_deletes_metadata() -> Result<()> {
2362 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2363
2364 let now = Timestamp::now();
2365 let url = Url::parse("https://www.mozilla.org/").unwrap();
2366 let other_url =
2367 Url::parse("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox")
2368 .unwrap();
2369 let start_timestamp = Timestamp(now.as_millis() - 1000_u64);
2370 let end_timestamp = Timestamp(now.as_millis() + 1000_u64);
2371 let observation1 = VisitObservation::new(url.clone())
2372 .with_at(start_timestamp)
2373 .with_title(Some(String::from("Test page 0")))
2374 .with_is_remote(false)
2375 .with_visit_type(VisitType::Link);
2376
2377 let observation2 = VisitObservation::new(other_url)
2378 .with_at(end_timestamp)
2379 .with_title(Some(String::from("Test page 1")))
2380 .with_is_remote(false)
2381 .with_visit_type(VisitType::Link);
2382
2383 apply_observation(&conn, observation1).expect("Should apply visit");
2384 apply_observation(&conn, observation2).expect("Should apply visit");
2385
2386 note_observation!(
2387 &conn,
2388 url "https://www.mozilla.org/",
2389 view_time Some(20000),
2390 search_term Some("mozilla firefox"),
2391 document_type Some(DocumentType::Regular),
2392 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2393 title None
2394 );
2395 assert_eq!(
2396 "https://www.mozilla.org/",
2397 get_latest_for_url(&conn, &url)?.unwrap().url
2398 );
2399 delete_visits_between(&conn, start_timestamp, end_timestamp)?;
2400 assert_eq!(None, get_latest_for_url(&conn, &url)?);
2401 Ok(())
2402 }
2403
2404 #[test]
2405 fn test_places_delete_triggers_with_bookmarks() {
2406 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2408
2409 let now = Timestamp::now();
2410 let url = Url::parse("https://www.mozilla.org/").unwrap();
2411 let parent_url =
2412 Url::parse("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox")
2413 .unwrap();
2414
2415 let observation1 = VisitObservation::new(url.clone())
2416 .with_at(now)
2417 .with_title(Some(String::from("Test page 0")))
2418 .with_is_remote(false)
2419 .with_visit_type(VisitType::Link);
2420
2421 let observation2 = VisitObservation::new(parent_url.clone())
2422 .with_at(now)
2423 .with_title(Some(String::from("Test page 1")))
2424 .with_is_remote(false)
2425 .with_visit_type(VisitType::Link);
2426
2427 apply_observation(&conn, observation1).expect("Should apply visit");
2428 apply_observation(&conn, observation2).expect("Should apply visit");
2429
2430 assert_table_size!(&conn, "moz_bookmarks", 5);
2431
2432 insert_bookmark(
2434 &conn,
2435 InsertableItem::Bookmark {
2436 b: InsertableBookmark {
2437 parent_guid: BookmarkRootGuid::Unfiled.into(),
2438 position: BookmarkPosition::Append,
2439 date_added: None,
2440 last_modified: None,
2441 guid: Some(SyncGuid::from("cccccccccccc")),
2442 url,
2443 title: None,
2444 },
2445 },
2446 )
2447 .expect("bookmark insert worked");
2448
2449 insert_bookmark(
2451 &conn,
2452 InsertableItem::Bookmark {
2453 b: InsertableBookmark {
2454 parent_guid: BookmarkRootGuid::Unfiled.into(),
2455 position: BookmarkPosition::Append,
2456 date_added: None,
2457 last_modified: None,
2458 guid: Some(SyncGuid::from("ccccccccccca")),
2459 url: parent_url,
2460 title: None,
2461 },
2462 },
2463 )
2464 .expect("bookmark insert worked");
2465
2466 assert_table_size!(&conn, "moz_bookmarks", 7);
2467 assert_table_size!(&conn, "moz_origins", 2);
2468
2469 note_observation!(
2470 &conn,
2471 url "https://www.mozilla.org/",
2472 view_time Some(20000),
2473 search_term Some("mozilla firefox"),
2474 document_type Some(DocumentType::Regular),
2475 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2476 title None
2477 );
2478
2479 assert_table_size!(&conn, "moz_origins", 2);
2480
2481 delete_everything(&conn).expect("places wipe succeeds");
2483
2484 assert_table_size!(&conn, "moz_places_metadata", 0);
2485 assert_table_size!(&conn, "moz_places_metadata_search_queries", 0);
2486 }
2487
2488 #[test]
2489 fn test_places_delete_triggers() {
2490 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2492
2493 let now = Timestamp::now();
2494 let observation1 = VisitObservation::new(Url::parse("https://www.mozilla.org/").unwrap())
2495 .with_at(now)
2496 .with_title(Some(String::from("Test page 1")))
2497 .with_is_remote(false)
2498 .with_visit_type(VisitType::Link);
2499 let observation2 =
2500 VisitObservation::new(Url::parse("https://www.mozilla.org/another/").unwrap())
2501 .with_at(Timestamp(now.as_millis() + 10000))
2502 .with_title(Some(String::from("Test page 3")))
2503 .with_is_remote(false)
2504 .with_visit_type(VisitType::Link);
2505 let observation3 =
2506 VisitObservation::new(Url::parse("https://www.mozilla.org/first/").unwrap())
2507 .with_at(Timestamp(now.as_millis() - 10000))
2508 .with_title(Some(String::from("Test page 0")))
2509 .with_is_remote(true)
2510 .with_visit_type(VisitType::Link);
2511 apply_observation(&conn, observation1).expect("Should apply visit");
2512 apply_observation(&conn, observation2).expect("Should apply visit");
2513 apply_observation(&conn, observation3).expect("Should apply visit");
2514
2515 note_observation!(
2516 &conn,
2517 url "https://www.mozilla.org/first/",
2518 view_time Some(20000),
2519 search_term None,
2520 document_type Some(DocumentType::Regular),
2521 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2522 title None
2523 );
2524
2525 note_observation!(
2526 &conn,
2527 url "https://www.mozilla.org/",
2528 view_time Some(20000),
2529 search_term None,
2530 document_type Some(DocumentType::Regular),
2531 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2532 title None
2533 );
2534
2535 note_observation!(
2536 &conn,
2537 url "https://www.mozilla.org/",
2538 view_time Some(20000),
2539 search_term Some("mozilla"),
2540 document_type Some(DocumentType::Regular),
2541 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2542 title None
2543 );
2544
2545 note_observation!(
2546 &conn,
2547 url "https://www.mozilla.org/",
2548 view_time Some(25000),
2549 search_term Some("firefox"),
2550 document_type Some(DocumentType::Media),
2551 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2552 title None
2553 );
2554
2555 note_observation!(
2556 &conn,
2557 url "https://www.mozilla.org/another/",
2558 view_time Some(20000),
2559 search_term Some("mozilla"),
2560 document_type Some(DocumentType::Regular),
2561 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2562 title None
2563 );
2564
2565 assert!(conn
2567 .try_query_one::<i64, _>(
2568 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
2569 rusqlite::named_params! { ":term": "firefox" },
2570 true
2571 )
2572 .expect("select works")
2573 .is_some());
2574
2575 delete_visits_between(
2577 &conn,
2578 Timestamp(now.as_millis() - 1000),
2579 Timestamp(now.as_millis() + 1000),
2580 )
2581 .expect("delete worked");
2582
2583 let meta1 =
2584 get_latest_for_url(&conn, &Url::parse("https://www.mozilla.org/").expect("url"))
2585 .expect("get worked");
2586 let meta2 = get_latest_for_url(
2587 &conn,
2588 &Url::parse("https://www.mozilla.org/another/").expect("url"),
2589 )
2590 .expect("get worked");
2591
2592 assert!(meta1.is_none(), "expected metadata to have been deleted");
2593 assert!(meta2.is_none(), "expected metadata to been deleted");
2597
2598 assert!(
2600 conn.try_query_one::<i64, _>(
2601 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
2602 rusqlite::named_params! { ":term": "mozilla" },
2603 true
2604 )
2605 .expect("select works")
2606 .is_none(),
2607 "search_query records with related metadata should have been deleted"
2608 );
2609
2610 assert!(
2612 conn.try_query_one::<i64, _>(
2613 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
2614 rusqlite::named_params! { ":term": "firefox" },
2615 true
2616 )
2617 .expect("select works")
2618 .is_none(),
2619 "search_query records without related metadata should have been deleted"
2620 );
2621
2622 delete_everything(&conn).expect("places wipe succeeds");
2624
2625 assert_table_size!(&conn, "moz_places_metadata", 0);
2626 assert_table_size!(&conn, "moz_places_metadata_search_queries", 0);
2627 }
2628
2629 #[test]
2630 fn test_delete_all_metadata_for_search() {
2631 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2632
2633 note_observation!(&conn,
2634 url "https://www.mozilla.org/1/",
2635 view_time None,
2636 search_term Some("search_term_1"),
2637 document_type None,
2638 referrer_url None,
2639 title None
2640 );
2641
2642 note_observation!(&conn,
2643 url "https://www.mozilla.org/2/",
2644 view_time None,
2645 search_term Some("search_term_2"),
2646 document_type None,
2647 referrer_url None,
2648 title None
2649 );
2650
2651 assert_table_size!(&conn, "moz_places_metadata", 2);
2652 assert_table_size!(&conn, "moz_places_metadata_search_queries", 2);
2653
2654 delete_all_metadata_for_search(&conn).expect("query ok");
2655
2656 assert_table_size!(&conn, "moz_places_metadata", 0);
2657 assert_table_size!(&conn, "moz_places_metadata_search_queries", 0);
2658 }
2659
2660 #[test]
2661 fn test_delete_all_metadata_for_search_only_deletes_search_metadata() {
2662 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2663
2664 note_observation!(&conn,
2671 url "https://www.mozilla.org/1/",
2672 view_time None,
2673 search_term Some("search_term_1"),
2674 document_type None,
2675 referrer_url None,
2676 title None
2677 );
2678
2679 note_observation!(
2680 &conn,
2681 url "https://www.mozilla.org/2/",
2682 view_time Some(20000),
2683 search_term None,
2684 document_type Some(DocumentType::Media),
2685 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2686 title None
2687 );
2688
2689 note_observation!(&conn,
2690 url "https://www.mozilla.org/3/",
2691 view_time None,
2692 search_term Some("search_term_2"),
2693 document_type None,
2694 referrer_url None,
2695 title None
2696 );
2697
2698 note_observation!(
2699 &conn,
2700 url "https://www.mozilla.org/4/",
2701 view_time Some(20000),
2702 search_term None,
2703 document_type Some(DocumentType::Regular),
2704 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2705 title None
2706 );
2707
2708 assert_eq!(4, get_since(&conn, 0).expect("get worked").len());
2709
2710 assert_table_size!(&conn, "moz_places_metadata", 4);
2711 assert_table_size!(&conn, "moz_places_metadata_search_queries", 2);
2712
2713 delete_all_metadata_for_search(&conn).expect("query ok");
2714
2715 assert_table_size!(&conn, "moz_places_metadata", 2);
2716 assert_table_size!(&conn, "moz_places_metadata_search_queries", 0);
2717 }
2718
2719 #[test]
2720 fn test_if_page_missing_behavior() {
2721 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2722
2723 note_observation!(
2724 &conn,
2725 NoteHistoryMetadataObservationOptions::new()
2726 .if_page_missing(HistoryMetadataPageMissingBehavior::IgnoreObservation),
2727 url "https://www.example.com/",
2728 view_time None,
2729 search_term None,
2730 document_type Some(DocumentType::Regular),
2731 referrer_url None,
2732 title None
2733 );
2734
2735 let observations = get_since(&conn, 0).expect("should get all metadata observations");
2736 assert_eq!(observations, &[]);
2737
2738 let visit_observation =
2739 VisitObservation::new(Url::parse("https://www.example.com/").unwrap())
2740 .with_at(Timestamp::now());
2741 apply_observation(&conn, visit_observation).expect("should apply visit observation");
2742
2743 note_observation!(
2744 &conn,
2745 NoteHistoryMetadataObservationOptions::new()
2746 .if_page_missing(HistoryMetadataPageMissingBehavior::IgnoreObservation),
2747 url "https://www.example.com/",
2748 view_time None,
2749 search_term None,
2750 document_type Some(DocumentType::Regular),
2751 referrer_url None,
2752 title None
2753 );
2754
2755 let observations = get_since(&conn, 0).expect("should get all metadata observations");
2756 assert_eq!(
2757 observations
2758 .into_iter()
2759 .map(|m| m.url)
2760 .collect::<Vec<String>>(),
2761 &["https://www.example.com/"]
2762 );
2763
2764 note_observation!(
2765 &conn,
2766 NoteHistoryMetadataObservationOptions::new()
2767 .if_page_missing(HistoryMetadataPageMissingBehavior::InsertPage),
2768 url "https://www.example.org/",
2769 view_time None,
2770 search_term None,
2771 document_type Some(DocumentType::Regular),
2772 referrer_url None,
2773 title None
2774 );
2775
2776 let observations = get_since(&conn, 0).expect("should get all metadata observations");
2777 assert_eq!(
2778 observations
2779 .into_iter()
2780 .map(|m| m.url)
2781 .collect::<Vec<String>>(),
2782 &[
2783 "https://www.example.org/", "https://www.example.com/",
2785 ],
2786 );
2787 }
2788}