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