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 QUERY_SQL: String = format!(
447 "{common_select_sql}
448 WHERE
449 p.url LIKE :query OR
450 p.title LIKE :query OR
451 search_term LIKE :query
452 ORDER BY total_view_time DESC
453 LIMIT :limit",
454 common_select_sql = COMMON_METADATA_SELECT
455 );
456}
457
458pub fn get_latest_for_url(db: &PlacesDb, url: &Url) -> Result<Option<HistoryMetadata>> {
459 let metadata = db.try_query_row(
460 GET_LATEST_SQL.as_str(),
461 &[(":url", &url.as_str())],
462 HistoryMetadata::from_row,
463 true,
464 )?;
465 Ok(metadata)
466}
467
468pub fn get_between(db: &PlacesDb, start: i64, end: i64) -> Result<Vec<HistoryMetadata>> {
469 db.query_rows_and_then_cached(
470 GET_BETWEEN_SQL.as_str(),
471 rusqlite::named_params! {
472 ":start": start,
473 ":end": end,
474 },
475 HistoryMetadata::from_row,
476 )
477}
478
479pub fn get_since(db: &PlacesDb, start: i64) -> Result<Vec<HistoryMetadata>> {
484 db.query_rows_and_then_cached(
485 GET_SINCE_SQL.as_str(),
486 rusqlite::named_params! {
487 ":start": start,
488 ":limit": MAX_QUERY_RESULTS,
489 },
490 HistoryMetadata::from_row,
491 )
492}
493
494pub fn get_most_recent(db: &PlacesDb, limit: i32) -> Result<Vec<HistoryMetadata>> {
500 db.query_rows_and_then_cached(
501 GET_SINCE_SQL.as_str(),
502 rusqlite::named_params! {
503 ":start": i64::MIN,
504 ":limit": limit,
505 },
506 HistoryMetadata::from_row,
507 )
508}
509
510pub fn get_highlights(
511 db: &PlacesDb,
512 weights: HistoryHighlightWeights,
513 limit: i32,
514) -> Result<Vec<HistoryHighlight>> {
515 db.query_rows_and_then_cached(
516 HIGHLIGHTS_QUERY,
517 rusqlite::named_params! {
518 ":view_time_weight": weights.view_time,
519 ":frequency_weight": weights.frequency,
520 ":limit": limit
521 },
522 HistoryHighlight::from_row,
523 )
524}
525
526pub fn query(db: &PlacesDb, query: &str, limit: i32) -> Result<Vec<HistoryMetadata>> {
527 db.query_rows_and_then_cached(
528 QUERY_SQL.as_str(),
529 rusqlite::named_params! {
530 ":query": format!("%{}%", query),
531 ":limit": limit
532 },
533 HistoryMetadata::from_row,
534 )
535}
536
537pub fn delete_older_than(db: &PlacesDb, older_than: i64) -> Result<()> {
538 db.execute_cached(
539 "DELETE FROM moz_places_metadata
540 WHERE updated_at < :older_than",
541 &[(":older_than", &older_than)],
542 )?;
543 Ok(())
544}
545
546pub fn delete_between(db: &PlacesDb, start: i64, end: i64) -> Result<()> {
547 db.execute_cached(
548 "DELETE FROM moz_places_metadata
549 WHERE updated_at > :start and updated_at < :end",
550 &[(":start", &start), (":end", &end)],
551 )?;
552 Ok(())
553}
554
555pub fn delete_all_metadata_for_page(db: &PlacesDb, place_id: RowId) -> Result<()> {
557 db.execute_cached(
558 "DELETE FROM moz_places_metadata
559 WHERE place_id = :place_id",
560 &[(":place_id", &place_id)],
561 )?;
562 Ok(())
563}
564
565pub fn delete_metadata(
566 db: &PlacesDb,
567 url: &Url,
568 referrer_url: Option<&Url>,
569 search_term: Option<&str>,
570) -> Result<()> {
571 let tx = db.begin_transaction()?;
572
573 let place_entry = PlaceEntry::fetch(url.as_str(), &tx, None)?;
579 let place_entry = match place_entry {
580 PlaceEntry::Existing(_) => place_entry,
581 PlaceEntry::CreateFor(_, _) => {
582 tx.rollback()?;
583 return Ok(());
584 }
585 };
586 let referrer_entry = match referrer_url {
587 Some(referrer_url) if !referrer_url.as_str().is_empty() => {
588 Some(PlaceEntry::fetch(referrer_url.as_str(), &tx, None)?)
589 }
590 _ => None,
591 };
592 let referrer_entry = match referrer_entry {
593 Some(PlaceEntry::Existing(_)) | None => referrer_entry,
594 Some(PlaceEntry::CreateFor(_, _)) => {
595 tx.rollback()?;
596 return Ok(());
597 }
598 };
599 let search_query_entry = match search_term {
600 Some(search_term) if !search_term.is_empty() => {
601 Some(SearchQueryEntry::from(search_term, &tx)?)
602 }
603 _ => None,
604 };
605 let search_query_entry = match search_query_entry {
606 Some(SearchQueryEntry::Existing(_)) | None => search_query_entry,
607 Some(SearchQueryEntry::CreateFor(_)) => {
608 tx.rollback()?;
609 return Ok(());
610 }
611 };
612
613 let sql = format!(
614 "DELETE FROM moz_places_metadata WHERE {} AND {} AND {}",
615 place_entry.to_where_arg("place_id"),
616 referrer_entry.to_where_arg("referrer_place_id"),
617 search_query_entry.to_where_arg("search_query_id")
618 );
619
620 tx.execute_cached(&sql, [])?;
621 tx.commit()?;
622
623 Ok(())
624}
625
626pub fn apply_metadata_observation(
627 db: &PlacesDb,
628 observation: HistoryMetadataObservation,
629 options: NoteHistoryMetadataObservationOptions,
630) -> Result<()> {
631 if let Some(view_time) = observation.view_time {
632 if view_time > 1000 * 60 * 60 * 24 {
641 return Err(InvalidMetadataObservation::ViewTimeTooLong.into());
642 }
643 }
644
645 let tx = db.begin_transaction()?;
650
651 let place_entry = PlaceEntry::fetch(&observation.url, &tx, observation.title.clone())?;
652 let result = apply_metadata_observation_impl(&tx, place_entry, observation, options);
653
654 super::delete_pending_temp_tables(db)?;
657 match result {
658 Ok(_) => tx.commit()?,
659 Err(_) => tx.rollback()?,
660 };
661
662 result
663}
664
665fn apply_metadata_observation_impl(
666 tx: &PlacesTransaction<'_>,
667 place_entry: PlaceEntry,
668 observation: HistoryMetadataObservation,
669 options: NoteHistoryMetadataObservationOptions,
670) -> Result<()> {
671 let referrer_entry = match observation.referrer_url {
672 Some(referrer_url) if !referrer_url.is_empty() => {
673 Some(PlaceEntry::fetch(&referrer_url, tx, None)?)
674 }
675 Some(_) | None => None,
676 };
677 let search_query_entry = match observation.search_term {
678 Some(search_term) if !search_term.is_empty() => {
679 Some(SearchQueryEntry::from(&search_term, tx)?)
680 }
681 Some(_) | None => None,
682 };
683
684 let compound_key = HistoryMetadataCompoundKey {
685 place_entry,
686 referrer_entry,
687 search_query_entry,
688 };
689
690 let observation = MetadataObservation {
691 document_type: observation.document_type,
692 view_time: observation.view_time,
693 };
694
695 let now = Timestamp::now().as_millis() as i64;
696 let newer_than = now - DEBOUNCE_WINDOW_MS;
697 let matching_metadata = compound_key.lookup(tx, newer_than)?;
698
699 match matching_metadata {
701 Some(metadata_id) => {
702 match observation {
704 MetadataObservation {
705 document_type: Some(dt),
706 view_time,
707 } => {
708 tx.execute_cached(
709 "UPDATE
710 moz_places_metadata
711 SET
712 document_type = :document_type,
713 total_view_time = total_view_time + :view_time_delta,
714 updated_at = :updated_at
715 WHERE id = :id",
716 rusqlite::named_params! {
717 ":id": metadata_id,
718 ":document_type": dt,
719 ":view_time_delta": view_time.unwrap_or(0),
720 ":updated_at": now
721 },
722 )?;
723 }
724 MetadataObservation {
725 document_type: None,
726 view_time,
727 } => {
728 tx.execute_cached(
729 "UPDATE
730 moz_places_metadata
731 SET
732 total_view_time = total_view_time + :view_time_delta,
733 updated_at = :updated_at
734 WHERE id = :id",
735 rusqlite::named_params! {
736 ":id": metadata_id,
737 ":view_time_delta": view_time.unwrap_or(0),
738 ":updated_at": now
739 },
740 )?;
741 }
742 }
743 Ok(())
744 }
745 None => insert_metadata_in_tx(tx, compound_key, observation, options),
746 }
747}
748
749fn insert_metadata_in_tx(
750 tx: &PlacesTransaction<'_>,
751 key: HistoryMetadataCompoundKey,
752 observation: MetadataObservation,
753 options: NoteHistoryMetadataObservationOptions,
754) -> Result<()> {
755 let now = Timestamp::now();
756
757 let referrer_place_id = match key.referrer_entry {
758 None => None,
759 Some(entry) => Some(entry.get_or_insert(tx)?),
760 };
761
762 let search_query_id = match key.search_query_entry {
763 None => None,
764 Some(entry) => Some(entry.get_or_insert(tx)?),
765 };
766
767 let place_id = match (key.place_entry, options.if_page_missing) {
770 (PlaceEntry::Existing(id), _) => id,
771 (PlaceEntry::CreateFor(_, _), HistoryMetadataPageMissingBehavior::IgnoreObservation) => {
772 return Ok(())
773 }
774 (
775 ref entry @ PlaceEntry::CreateFor(_, _),
776 HistoryMetadataPageMissingBehavior::InsertPage,
777 ) => entry.get_or_insert(tx)?,
778 };
779
780 let sql = "INSERT INTO moz_places_metadata
781 (place_id, created_at, updated_at, total_view_time, search_query_id, document_type, referrer_place_id)
782 VALUES
783 (:place_id, :created_at, :updated_at, :total_view_time, :search_query_id, :document_type, :referrer_place_id)";
784
785 tx.execute_cached(
786 sql,
787 &[
788 (":place_id", &place_id as &dyn rusqlite::ToSql),
789 (":created_at", &now),
790 (":updated_at", &now),
791 (":search_query_id", &search_query_id),
792 (":referrer_place_id", &referrer_place_id),
793 (
794 ":document_type",
795 &observation.document_type.unwrap_or(DocumentType::Regular),
796 ),
797 (":total_view_time", &observation.view_time.unwrap_or(0)),
798 ],
799 )?;
800
801 Ok(())
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807 use crate::api::places_api::ConnectionType;
808 use crate::observation::VisitObservation;
809 use crate::storage::bookmarks::{
810 get_raw_bookmark, insert_bookmark, BookmarkPosition, BookmarkRootGuid, InsertableBookmark,
811 InsertableItem,
812 };
813 use crate::storage::fetch_page_info;
814 use crate::storage::history::{
815 apply_observation, delete_everything, delete_visits_between, delete_visits_for,
816 get_visit_count, url_to_guid,
817 };
818 use crate::types::VisitType;
819 use crate::VisitTransitionSet;
820 use std::{thread, time};
821
822 fn bump_clock() {
827 thread::sleep(time::Duration::from_millis(10));
828 }
829
830 macro_rules! assert_table_size {
831 ($conn:expr, $table:expr, $count:expr) => {
832 assert_eq!(
833 $count,
834 $conn
835 .try_query_one::<i64, _>(
836 format!("SELECT count(*) FROM {table}", table = $table).as_str(),
837 [],
838 true
839 )
840 .expect("select works")
841 .expect("got count")
842 );
843 };
844 }
845
846 macro_rules! assert_history_metadata_record {
847 ($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) => {
848 assert_eq!(String::from($url), $record.url, "url must match");
849 assert_eq!($tvt, $record.total_view_time, "total_view_time must match");
850 assert_eq!($document_type, $record.document_type, "is_media must match");
851
852 let meta = $record.clone(); match $search_term as Option<&str> {
855 Some(t) => assert_eq!(
856 String::from(t),
857 meta.search_term.expect("search_term must be Some"),
858 "search_term must match"
859 ),
860 None => assert_eq!(
861 true,
862 meta.search_term.is_none(),
863 "search_term expected to be None"
864 ),
865 };
866 match $referrer_url as Option<&str> {
867 Some(t) => assert_eq!(
868 String::from(t),
869 meta.referrer_url.expect("referrer_url must be Some"),
870 "referrer_url must match"
871 ),
872 None => assert_eq!(
873 true,
874 meta.referrer_url.is_none(),
875 "referrer_url expected to be None"
876 ),
877 };
878 match $title as Option<&str> {
879 Some(t) => assert_eq!(
880 String::from(t),
881 meta.title.expect("title must be Some"),
882 "title must match"
883 ),
884 None => assert_eq!(true, meta.title.is_none(), "title expected to be None"),
885 };
886 match $preview_image_url as Option<&str> {
887 Some(t) => assert_eq!(
888 String::from(t),
889 meta.preview_image_url
890 .expect("preview_image_url must be Some"),
891 "preview_image_url must match"
892 ),
893 None => assert_eq!(
894 true,
895 meta.preview_image_url.is_none(),
896 "preview_image_url expected to be None"
897 ),
898 };
899 };
900 }
901
902 macro_rules! assert_total_after_observation {
903 ($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) => {
904 note_observation!($conn,
905 url $url,
906 view_time $view_time,
907 search_term $search_term,
908 document_type $document_type,
909 referrer_url $referrer_url,
910 title $title
911 );
912
913 assert_table_size!($conn, "moz_places_metadata", $total_records);
914 let updated = get_latest_for_url($conn, &Url::parse($url).unwrap()).unwrap().unwrap();
915 assert_eq!($total_view_time, updated.total_view_time, "total view time must match");
916 }
917 }
918
919 macro_rules! note_observation {
920 ($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) => {
921 note_observation!(
922 $conn,
923 NoteHistoryMetadataObservationOptions::new()
924 .if_page_missing(HistoryMetadataPageMissingBehavior::InsertPage),
925 url $url,
926 view_time $view_time,
927 search_term $search_term,
928 document_type $document_type,
929 referrer_url $referrer_url,
930 title $title
931 )
932 };
933 ($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) => {
934 apply_metadata_observation(
935 $conn,
936 HistoryMetadataObservation {
937 url: String::from($url),
938 view_time: $view_time,
939 search_term: $search_term.map(|s: &str| s.to_string()),
940 document_type: $document_type,
941 referrer_url: $referrer_url.map(|s: &str| s.to_string()),
942 title: $title.map(|s: &str| s.to_string()),
943 },
944 $options,
945 )
946 .unwrap();
947 };
948 }
949
950 macro_rules! assert_after_observation {
951 ($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) => {
952 assert_total_after_observation!($conn,
954 total_records_after $total_records,
955 total_view_time_after $total_view_time,
956 url $url,
957 view_time $view_time,
958 search_term $search_term,
959 document_type $document_type,
960 referrer_url $referrer_url,
961 title $title
962 );
963
964 let m = get_latest_for_url(
965 $conn,
966 &Url::parse(&String::from($url)).unwrap(),
967 )
968 .unwrap()
969 .unwrap();
970 #[allow(clippy::redundant_closure_call)]
971 $assertion(m);
972 }
973 }
974
975 #[test]
976 fn test_note_observation() {
977 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).unwrap();
978
979 assert_table_size!(&conn, "moz_places_metadata", 0);
980
981 assert_total_after_observation!(&conn,
982 total_records_after 1,
983 total_view_time_after 1500,
984 url "http://mozilla.com/",
985 view_time Some(1500),
986 search_term None,
987 document_type Some(DocumentType::Regular),
988 referrer_url None,
989 title None
990 );
991
992 assert_total_after_observation!(&conn,
994 total_records_after 1,
995 total_view_time_after 2500,
996 url "http://mozilla.com/",
997 view_time Some(1000),
998 search_term None,
999 document_type Some(DocumentType::Regular),
1000 referrer_url None,
1001 title None
1002 );
1003
1004 assert_total_after_observation!(&conn,
1006 total_records_after 1,
1007 total_view_time_after 3500,
1008 url "http://mozilla.com/",
1009 view_time Some(1000),
1010 search_term None,
1011 document_type Some(DocumentType::Media),
1012 referrer_url None,
1013 title None
1014 );
1015
1016 assert_total_after_observation!(&conn,
1018 total_records_after 2,
1019 total_view_time_after 2000,
1020 url "http://mozilla.com/",
1021 view_time Some(2000),
1022 search_term None,
1023 document_type Some(DocumentType::Media),
1024 referrer_url Some("https://news.website"),
1025 title None
1026 );
1027
1028 assert_total_after_observation!(&conn,
1030 total_records_after 3,
1031 total_view_time_after 1100,
1032 url "http://mozilla.com/",
1033 view_time Some(1100),
1034 search_term Some("firefox"),
1035 document_type Some(DocumentType::Media),
1036 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=firefox"),
1037 title None
1038 );
1039
1040 assert_total_after_observation!(&conn,
1042 total_records_after 3,
1043 total_view_time_after 6100,
1044 url "http://mozilla.com/",
1045 view_time Some(5000),
1046 search_term Some("firefox"),
1047 document_type Some(DocumentType::Media),
1048 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=firefox"),
1049 title None
1050 );
1051
1052 assert_total_after_observation!(&conn,
1054 total_records_after 4,
1055 total_view_time_after 3000,
1056 url "http://mozilla.com/another",
1057 view_time Some(3000),
1058 search_term None,
1059 document_type Some(DocumentType::Regular),
1060 referrer_url Some("https://news.website/tech"),
1061 title None
1062 );
1063
1064 assert_total_after_observation!(&conn,
1066 total_records_after 5,
1067 total_view_time_after 100000,
1068 url "https://www.youtube.com/watch?v=tpiyEe_CqB4",
1069 view_time Some(100000),
1070 search_term Some("cute cat"),
1071 document_type Some(DocumentType::Media),
1072 referrer_url Some("https://www.youtube.com/results?search_query=cute+cat"),
1073 title None
1074 );
1075
1076 assert_total_after_observation!(&conn,
1078 total_records_after 6,
1079 total_view_time_after 80000,
1080 url "https://www.youtube.com/watch?v=daff43jif3",
1081 view_time Some(80000),
1082 search_term Some(""),
1083 document_type Some(DocumentType::Media),
1084 referrer_url Some(""),
1085 title None
1086 );
1087
1088 assert_total_after_observation!(&conn,
1089 total_records_after 6,
1090 total_view_time_after 90000,
1091 url "https://www.youtube.com/watch?v=daff43jif3",
1092 view_time Some(10000),
1093 search_term None,
1094 document_type Some(DocumentType::Media),
1095 referrer_url None,
1096 title None
1097 );
1098
1099 assert_total_after_observation!(&conn,
1101 total_records_after 7,
1102 total_view_time_after 0,
1103 url "https://www.youtube.com/watch?v=fds32fds",
1104 view_time None,
1105 search_term None,
1106 document_type Some(DocumentType::Media),
1107 referrer_url None,
1108 title None
1109 );
1110
1111 assert_total_after_observation!(&conn,
1113 total_records_after 7,
1114 total_view_time_after 1338,
1115 url "https://www.youtube.com/watch?v=fds32fds",
1116 view_time Some(1338),
1117 search_term None,
1118 document_type None,
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 2000,
1127 url "https://www.youtube.com/watch?v=fds32fds",
1128 view_time Some(662),
1129 search_term None,
1130 document_type None,
1131 referrer_url None,
1132 title None
1133 );
1134
1135 assert_after_observation!(&conn,
1138 total_records_after 8,
1139 total_view_time_after 662,
1140 url "https://www.youtube.com/watch?v=dasdg34d",
1141 view_time Some(662),
1142 search_term None,
1143 document_type None,
1144 referrer_url None,
1145 title None,
1146 assertion |m: HistoryMetadata| { assert_eq!(DocumentType::Regular, m.document_type) }
1147 );
1148
1149 assert_after_observation!(&conn,
1150 total_records_after 8,
1151 total_view_time_after 662,
1152 url "https://www.youtube.com/watch?v=dasdg34d",
1153 view_time None,
1154 search_term None,
1155 document_type Some(DocumentType::Media),
1156 referrer_url None,
1157 title None,
1158 assertion |m: HistoryMetadata| { assert_eq!(DocumentType::Media, m.document_type) }
1159 );
1160
1161 assert_after_observation!(&conn,
1163 total_records_after 8,
1164 total_view_time_after 675,
1165 url "https://www.youtube.com/watch?v=dasdg34d",
1166 view_time Some(13),
1167 search_term None,
1168 document_type None,
1169 referrer_url None,
1170 title None,
1171 assertion |m: HistoryMetadata| { assert_eq!(DocumentType::Media, m.document_type) }
1172 );
1173
1174 assert_after_observation!(&conn,
1176 total_records_after 9,
1177 total_view_time_after 13,
1178 url "https://www.youtube.com/watch?v=dasdsada",
1179 view_time Some(13),
1180 search_term None,
1181 document_type None,
1182 referrer_url None,
1183 title Some("hello!"),
1184 assertion |m: HistoryMetadata| { assert_eq!(Some(String::from("hello!")), m.title) }
1185 );
1186
1187 assert_after_observation!(&conn,
1189 total_records_after 9,
1190 total_view_time_after 26,
1191 url "https://www.youtube.com/watch?v=dasdsada",
1192 view_time Some(13),
1193 search_term None,
1194 document_type None,
1195 referrer_url None,
1196 title Some("world!"),
1197 assertion |m: HistoryMetadata| { assert_eq!(Some(String::from("hello!")), m.title) }
1198 );
1199 }
1200
1201 #[test]
1202 fn test_note_observation_invalid_view_time() {
1203 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1204
1205 note_observation!(&conn,
1206 url "https://www.mozilla.org/",
1207 view_time None,
1208 search_term None,
1209 document_type Some(DocumentType::Regular),
1210 referrer_url None,
1211 title None
1212 );
1213
1214 assert!(apply_metadata_observation(
1216 &conn,
1217 HistoryMetadataObservation {
1218 url: String::from("https://www.mozilla.org"),
1219 view_time: Some(1000 * 60 * 60 * 24 * 2),
1220 search_term: None,
1221 document_type: None,
1222 referrer_url: None,
1223 title: None
1224 },
1225 NoteHistoryMetadataObservationOptions::new(),
1226 )
1227 .is_err());
1228
1229 assert!(apply_metadata_observation(
1231 &conn,
1232 HistoryMetadataObservation {
1233 url: String::from("https://www.mozilla.org"),
1234 view_time: Some(1000 * 60 * 60 * 12),
1235 search_term: None,
1236 document_type: None,
1237 referrer_url: None,
1238 title: None
1239 },
1240 NoteHistoryMetadataObservationOptions::new(),
1241 )
1242 .is_ok());
1243 }
1244
1245 #[test]
1246 fn test_get_between() {
1247 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1248
1249 assert_eq!(0, get_between(&conn, 0, 0).unwrap().len());
1250
1251 let beginning = Timestamp::now().as_millis() as i64;
1252 note_observation!(&conn,
1253 url "http://mozilla.com/another",
1254 view_time Some(3000),
1255 search_term None,
1256 document_type Some(DocumentType::Regular),
1257 referrer_url Some("https://news.website/tech"),
1258 title None
1259 );
1260 let after_meta1 = Timestamp::now().as_millis() as i64;
1261
1262 assert_eq!(0, get_between(&conn, 0, beginning - 1).unwrap().len());
1263 assert_eq!(1, get_between(&conn, 0, after_meta1).unwrap().len());
1264
1265 bump_clock();
1266
1267 note_observation!(&conn,
1268 url "http://mozilla.com/video/",
1269 view_time Some(1000),
1270 search_term None,
1271 document_type Some(DocumentType::Media),
1272 referrer_url None,
1273 title None
1274 );
1275 let after_meta2 = Timestamp::now().as_millis() as i64;
1276
1277 assert_eq!(1, get_between(&conn, beginning, after_meta1).unwrap().len());
1278 assert_eq!(2, get_between(&conn, beginning, after_meta2).unwrap().len());
1279 assert_eq!(
1280 1,
1281 get_between(&conn, after_meta1, after_meta2).unwrap().len()
1282 );
1283 assert_eq!(
1284 0,
1285 get_between(&conn, after_meta2, after_meta2 + 1)
1286 .unwrap()
1287 .len()
1288 );
1289 }
1290
1291 #[test]
1292 fn test_get_since() {
1293 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1294
1295 assert_eq!(0, get_since(&conn, 0).unwrap().len());
1296
1297 let beginning = Timestamp::now().as_millis() as i64;
1298 note_observation!(&conn,
1299 url "http://mozilla.com/another",
1300 view_time Some(3000),
1301 search_term None,
1302 document_type Some(DocumentType::Regular),
1303 referrer_url Some("https://news.website/tech"),
1304 title None
1305 );
1306 let after_meta1 = Timestamp::now().as_millis() as i64;
1307
1308 assert_eq!(1, get_since(&conn, 0).unwrap().len());
1309 assert_eq!(1, get_since(&conn, beginning).unwrap().len());
1310 assert_eq!(0, get_since(&conn, after_meta1).unwrap().len());
1311
1312 note_observation!(&conn,
1315 url "http://mozilla.com/video/",
1316 view_time Some(1000),
1317 search_term None,
1318 document_type Some(DocumentType::Media),
1319 referrer_url None,
1320 title None
1321 );
1322 let after_meta2 = Timestamp::now().as_millis() as i64;
1323 assert_eq!(2, get_since(&conn, beginning).unwrap().len());
1324 assert_eq!(1, get_since(&conn, after_meta1).unwrap().len());
1325 assert_eq!(0, get_since(&conn, after_meta2).unwrap().len());
1326 }
1327
1328 #[test]
1329 fn test_get_most_recent_empty() {
1330 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1331 let rows = get_most_recent(&conn, 5).expect("query ok");
1332 assert!(rows.is_empty());
1333 }
1334
1335 #[test]
1336 fn test_get_most_recent_orders_and_limits_same_observation() {
1337 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1338
1339 note_observation!(&conn,
1340 url "https://example.com/1",
1341 view_time Some(10),
1342 search_term None,
1343 document_type Some(DocumentType::Regular),
1344 referrer_url None,
1345 title None
1346 );
1347
1348 bump_clock();
1349
1350 note_observation!(&conn,
1351 url "https://example.com/1",
1352 view_time Some(10),
1353 search_term None,
1354 document_type Some(DocumentType::Regular),
1355 referrer_url None,
1356 title None
1357 );
1358
1359 bump_clock();
1360
1361 note_observation!(&conn,
1362 url "https://example.com/1",
1363 view_time Some(10),
1364 search_term None,
1365 document_type Some(DocumentType::Regular),
1366 referrer_url None,
1367 title None
1368 );
1369
1370 let most_recents1 = get_most_recent(&conn, 1).expect("query ok");
1372 assert_eq!(most_recents1.len(), 1);
1373 assert_eq!(most_recents1[0].url, "https://example.com/1");
1374
1375 let most_recents2 = get_most_recent(&conn, 3).expect("query ok");
1377 assert_eq!(most_recents2.len(), 1);
1378 assert_eq!(most_recents2[0].url, "https://example.com/1");
1379
1380 let most_recents3 = get_most_recent(&conn, 10).expect("query ok");
1382 assert_eq!(most_recents3.len(), 1);
1383 assert_eq!(most_recents3[0].url, "https://example.com/1");
1384 }
1385
1386 #[test]
1387 fn test_get_most_recent_orders_and_limits_different_observations() {
1388 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1389
1390 note_observation!(&conn,
1391 url "https://example.com/1",
1392 view_time Some(10),
1393 search_term None,
1394 document_type Some(DocumentType::Regular),
1395 referrer_url None,
1396 title None
1397 );
1398
1399 bump_clock();
1400
1401 note_observation!(&conn,
1402 url "https://example.com/2",
1403 view_time Some(20),
1404 search_term None,
1405 document_type Some(DocumentType::Regular),
1406 referrer_url None,
1407 title None
1408 );
1409
1410 bump_clock();
1411
1412 note_observation!(&conn,
1413 url "https://example.com/3",
1414 view_time Some(30),
1415 search_term None,
1416 document_type Some(DocumentType::Regular),
1417 referrer_url None,
1418 title None
1419 );
1420
1421 let most_recents1 = get_most_recent(&conn, 1).expect("query ok");
1423 assert_eq!(most_recents1.len(), 1);
1424 assert_eq!(most_recents1[0].url, "https://example.com/3");
1425
1426 let most_recents2 = get_most_recent(&conn, 2).expect("query ok");
1428 assert_eq!(most_recents2.len(), 2);
1429 assert_eq!(most_recents2[0].url, "https://example.com/3");
1430 assert_eq!(most_recents2[1].url, "https://example.com/2");
1431
1432 let most_recents3 = get_most_recent(&conn, 10).expect("query ok");
1434 assert_eq!(most_recents3.len(), 3);
1435 assert_eq!(most_recents3[0].url, "https://example.com/3");
1436 assert_eq!(most_recents3[1].url, "https://example.com/2");
1437 assert_eq!(most_recents3[2].url, "https://example.com/1");
1438 }
1439
1440 #[test]
1441 fn test_get_most_recent_negative_limit() {
1442 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1443
1444 note_observation!(&conn,
1445 url "https://example.com/1",
1446 view_time Some(10),
1447 search_term None,
1448 document_type Some(DocumentType::Regular),
1449 referrer_url None,
1450 title None
1451 );
1452
1453 bump_clock();
1454
1455 note_observation!(&conn,
1456 url "https://example.com/2",
1457 view_time Some(10),
1458 search_term None,
1459 document_type Some(DocumentType::Regular),
1460 referrer_url None,
1461 title None
1462 );
1463
1464 bump_clock();
1465
1466 note_observation!(&conn,
1467 url "https://example.com/3",
1468 view_time Some(10),
1469 search_term None,
1470 document_type Some(DocumentType::Regular),
1471 referrer_url None,
1472 title None
1473 );
1474
1475 let most_recents = get_most_recent(&conn, -1).expect("query ok");
1477 assert_eq!(most_recents.len(), 3);
1478 assert_eq!(most_recents[0].url, "https://example.com/3");
1479 assert_eq!(most_recents[1].url, "https://example.com/2");
1480 assert_eq!(most_recents[2].url, "https://example.com/1");
1481 }
1482
1483 #[test]
1484 fn test_get_highlights() {
1485 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1486
1487 assert_eq!(
1489 0,
1490 get_highlights(
1491 &conn,
1492 HistoryHighlightWeights {
1493 view_time: 1.0,
1494 frequency: 1.0
1495 },
1496 10
1497 )
1498 .unwrap()
1499 .len()
1500 );
1501
1502 apply_observation(
1504 &conn,
1505 VisitObservation::new(
1506 Url::parse("https://www.reddit.com/r/climbing").expect("Should parse URL"),
1507 )
1508 .with_visit_type(VisitType::Link)
1509 .with_at(Timestamp::now()),
1510 )
1511 .expect("Should apply observation");
1512 assert_eq!(
1513 0,
1514 get_highlights(
1515 &conn,
1516 HistoryHighlightWeights {
1517 view_time: 1.0,
1518 frequency: 1.0
1519 },
1520 10
1521 )
1522 .unwrap()
1523 .len()
1524 );
1525
1526 note_observation!(&conn,
1528 url "http://mozilla.com/1",
1529 view_time Some(1000),
1530 search_term None,
1531 document_type Some(DocumentType::Regular),
1532 referrer_url Some("https://news.website/tech"),
1533 title None
1534 );
1535
1536 note_observation!(&conn,
1537 url "http://mozilla.com/1",
1538 view_time Some(1000),
1539 search_term None,
1540 document_type Some(DocumentType::Regular),
1541 referrer_url Some("https://news.website/tech"),
1542 title None
1543 );
1544
1545 note_observation!(&conn,
1546 url "http://mozilla.com/1",
1547 view_time Some(1000),
1548 search_term None,
1549 document_type Some(DocumentType::Regular),
1550 referrer_url Some("https://news.website/tech"),
1551 title None
1552 );
1553
1554 note_observation!(&conn,
1556 url "http://mozilla.com/2",
1557 view_time Some(3500),
1558 search_term None,
1559 document_type Some(DocumentType::Regular),
1560 referrer_url Some("https://news.website/tech"),
1561 title None
1562 );
1563
1564 let even_weights = HistoryHighlightWeights {
1571 view_time: 1.0,
1572 frequency: 1.0,
1573 };
1574 let highlights1 = get_highlights(&conn, even_weights.clone(), 10).unwrap();
1575 assert_eq!(2, highlights1.len());
1576 assert_eq!("http://mozilla.com/2", highlights1[0].url);
1577
1578 let frequency_heavy_weights = HistoryHighlightWeights {
1580 view_time: 1.0,
1581 frequency: 100.0,
1582 };
1583 let highlights2 = get_highlights(&conn, frequency_heavy_weights, 10).unwrap();
1584 assert_eq!(2, highlights2.len());
1585 assert_eq!("http://mozilla.com/2", highlights2[0].url);
1586
1587 note_observation!(&conn,
1591 url "http://mozilla.com/1",
1592 view_time Some(100),
1593 search_term Some("test search"),
1594 document_type Some(DocumentType::Regular),
1595 referrer_url Some("https://news.website/tech"),
1596 title None
1597 );
1598
1599 let highlights3 = get_highlights(&conn, even_weights, 10).unwrap();
1601 assert_eq!(2, highlights3.len());
1602 assert_eq!("http://mozilla.com/1", highlights3[0].url);
1603
1604 let view_time_heavy_weights = HistoryHighlightWeights {
1607 view_time: 6.0,
1608 frequency: 1.0,
1609 };
1610 let highlights4 = get_highlights(&conn, view_time_heavy_weights, 10).unwrap();
1611 assert_eq!(2, highlights4.len());
1612 assert_eq!("http://mozilla.com/2", highlights4[0].url);
1613 }
1614
1615 #[test]
1616 fn test_get_highlights_no_viewtime() {
1617 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1618
1619 note_observation!(&conn,
1621 url "http://mozilla.com/1",
1622 view_time Some(0),
1623 search_term None,
1624 document_type Some(DocumentType::Regular),
1625 referrer_url Some("https://news.website/tech"),
1626 title None
1627 );
1628 let highlights = get_highlights(
1629 &conn,
1630 HistoryHighlightWeights {
1631 view_time: 1.0,
1632 frequency: 1.0,
1633 },
1634 2,
1635 )
1636 .unwrap();
1637 assert_eq!(highlights.len(), 1);
1638 assert_eq!(highlights[0].score, 0.0);
1639 }
1640
1641 #[test]
1642 fn test_query() {
1643 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1644 let now = Timestamp::now();
1645
1646 let observation1 = VisitObservation::new(Url::parse("https://www.cbc.ca/news/politics/federal-budget-2021-freeland-zimonjic-1.5991021").unwrap())
1648 .with_at(now)
1649 .with_title(Some(String::from("Budget vows to build 'for the long term' as it promises child care cash, projects massive deficits | CBC News")))
1650 .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()))
1651 .with_is_remote(false)
1652 .with_visit_type(VisitType::Link);
1653 apply_observation(&conn, observation1).unwrap();
1654
1655 note_observation!(
1656 &conn,
1657 url "https://www.cbc.ca/news/politics/federal-budget-2021-freeland-zimonjic-1.5991021",
1658 view_time Some(20000),
1659 search_term Some("cbc federal budget 2021"),
1660 document_type Some(DocumentType::Regular),
1661 referrer_url Some("https://yandex.ru/search/?text=cbc%20federal%20budget%202021&lr=21512"),
1662 title None
1663 );
1664
1665 note_observation!(
1666 &conn,
1667 url "https://stackoverflow.com/questions/37777675/how-to-create-a-formatted-string-out-of-a-literal-in-rust",
1668 view_time Some(20000),
1669 search_term Some("rust string format"),
1670 document_type Some(DocumentType::Regular),
1671 referrer_url Some("https://yandex.ru/search/?lr=21512&text=rust%20string%20format"),
1672 title None
1673 );
1674
1675 note_observation!(
1676 &conn,
1677 url "https://www.sqlite.org/lang_corefunc.html#instr",
1678 view_time Some(20000),
1679 search_term Some("sqlite like"),
1680 document_type Some(DocumentType::Regular),
1681 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=sqlite+like"),
1682 title None
1683 );
1684
1685 note_observation!(
1686 &conn,
1687 url "https://www.youtube.com/watch?v=tpiyEe_CqB4",
1688 view_time Some(100000),
1689 search_term Some("cute cat"),
1690 document_type Some(DocumentType::Media),
1691 referrer_url Some("https://www.youtube.com/results?search_query=cute+cat"),
1692 title None
1693 );
1694
1695 let meta = query(&conn, "child care", 10).expect("query should work");
1697 assert_eq!(1, meta.len(), "expected exactly one result");
1698 assert_history_metadata_record!(meta[0],
1699 url "https://www.cbc.ca/news/politics/federal-budget-2021-freeland-zimonjic-1.5991021",
1700 total_time 20000,
1701 search_term Some("cbc federal budget 2021"),
1702 document_type DocumentType::Regular,
1703 referrer_url Some("https://yandex.ru/search/?text=cbc%20federal%20budget%202021&lr=21512"),
1704 title Some("Budget vows to build 'for the long term' as it promises child care cash, projects massive deficits | CBC News"),
1705 preview_image_url Some("https://i.cbc.ca/1.5993583.1618861792!/cpImage/httpImage/image.jpg_gen/derivatives/16x9_620/fedbudget-20210419.jpg")
1706 );
1707
1708 let meta = query(&conn, "string format", 10).expect("query should work");
1710 assert_eq!(1, meta.len(), "expected exactly one result");
1711 assert_history_metadata_record!(meta[0],
1712 url "https://stackoverflow.com/questions/37777675/how-to-create-a-formatted-string-out-of-a-literal-in-rust",
1713 total_time 20000,
1714 search_term Some("rust string format"),
1715 document_type DocumentType::Regular,
1716 referrer_url Some("https://yandex.ru/search/?lr=21512&text=rust%20string%20format"),
1717 title None,
1718 preview_image_url None
1719 );
1720
1721 let meta = query(&conn, "instr", 10).expect("query should work");
1723 assert_history_metadata_record!(meta[0],
1724 url "https://www.sqlite.org/lang_corefunc.html#instr",
1725 total_time 20000,
1726 search_term Some("sqlite like"),
1727 document_type DocumentType::Regular,
1728 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=sqlite+like"),
1729 title None,
1730 preview_image_url None
1731 );
1732
1733 let meta = query(&conn, "youtube", 10).expect("query should work");
1735 assert_history_metadata_record!(meta[0],
1736 url "https://www.youtube.com/watch?v=tpiyEe_CqB4",
1737 total_time 100000,
1738 search_term Some("cute cat"),
1739 document_type DocumentType::Media,
1740 referrer_url Some("https://www.youtube.com/results?search_query=cute+cat"),
1741 title None,
1742 preview_image_url None
1743 );
1744 }
1745
1746 #[test]
1747 fn test_delete_metadata() {
1748 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1749
1750 note_observation!(&conn,
1757 url "http://mozilla.com/1",
1758 view_time Some(20000),
1759 search_term Some("1 with search"),
1760 document_type Some(DocumentType::Regular),
1761 referrer_url Some("http://mozilla.com/"),
1762 title None
1763 );
1764
1765 note_observation!(&conn,
1766 url "http://mozilla.com/1",
1767 view_time Some(20000),
1768 search_term Some("1 with search"),
1769 document_type Some(DocumentType::Regular),
1770 referrer_url None,
1771 title None
1772 );
1773
1774 note_observation!(&conn,
1775 url "http://mozilla.com/1",
1776 view_time Some(20000),
1777 search_term None,
1778 document_type Some(DocumentType::Regular),
1779 referrer_url Some("http://mozilla.com/"),
1780 title None
1781 );
1782
1783 note_observation!(&conn,
1784 url "http://mozilla.com/1",
1785 view_time Some(20000),
1786 search_term None,
1787 document_type Some(DocumentType::Regular),
1788 referrer_url None,
1789 title None
1790 );
1791
1792 note_observation!(&conn,
1793 url "http://mozilla.com/2",
1794 view_time Some(20000),
1795 search_term None,
1796 document_type Some(DocumentType::Regular),
1797 referrer_url None,
1798 title None
1799 );
1800
1801 note_observation!(&conn,
1802 url "http://mozilla.com/2",
1803 view_time Some(20000),
1804 search_term None,
1805 document_type Some(DocumentType::Regular),
1806 referrer_url Some("http://mozilla.com/"),
1807 title None
1808 );
1809
1810 bump_clock();
1811 note_observation!(&conn,
1813 url "http://mozilla.com/2",
1814 view_time Some(20000),
1815 search_term None,
1816 document_type Some(DocumentType::Regular),
1817 referrer_url Some("http://mozilla.com/"),
1818 title None
1819 );
1820
1821 assert_eq!(6, get_since(&conn, 0).expect("get worked").len());
1822 delete_metadata(
1823 &conn,
1824 &Url::parse("http://mozilla.com/1").unwrap(),
1825 None,
1826 None,
1827 )
1828 .expect("delete metadata");
1829 assert_eq!(5, get_since(&conn, 0).expect("get worked").len());
1830
1831 delete_metadata(
1832 &conn,
1833 &Url::parse("http://mozilla.com/1").unwrap(),
1834 Some(&Url::parse("http://mozilla.com/").unwrap()),
1835 None,
1836 )
1837 .expect("delete metadata");
1838 assert_eq!(4, get_since(&conn, 0).expect("get worked").len());
1839
1840 delete_metadata(
1841 &conn,
1842 &Url::parse("http://mozilla.com/1").unwrap(),
1843 Some(&Url::parse("http://mozilla.com/").unwrap()),
1844 Some("1 with search"),
1845 )
1846 .expect("delete metadata");
1847 assert_eq!(3, get_since(&conn, 0).expect("get worked").len());
1848
1849 delete_metadata(
1850 &conn,
1851 &Url::parse("http://mozilla.com/1").unwrap(),
1852 None,
1853 Some("1 with search"),
1854 )
1855 .expect("delete metadata");
1856 assert_eq!(2, get_since(&conn, 0).expect("get worked").len());
1857
1858 delete_metadata(
1860 &conn,
1861 &Url::parse("http://mozilla.com/2").unwrap(),
1862 Some(&Url::parse("http://wrong-referrer.com").unwrap()),
1863 Some("2 with search"),
1864 )
1865 .expect("delete metadata");
1866 assert_eq!(2, get_since(&conn, 0).expect("get worked").len());
1867 }
1868
1869 #[test]
1870 fn test_delete_older_than() {
1871 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1872
1873 let beginning = Timestamp::now().as_millis() as i64;
1874
1875 note_observation!(&conn,
1876 url "http://mozilla.com/1",
1877 view_time Some(20000),
1878 search_term None,
1879 document_type Some(DocumentType::Regular),
1880 referrer_url None,
1881 title None
1882 );
1883 let after_meta1 = Timestamp::now().as_millis() as i64;
1884
1885 bump_clock();
1886
1887 note_observation!(&conn,
1888 url "http://mozilla.com/2",
1889 view_time Some(20000),
1890 search_term None,
1891 document_type Some(DocumentType::Regular),
1892 referrer_url None,
1893 title None
1894 );
1895
1896 bump_clock();
1897
1898 note_observation!(&conn,
1899 url "http://mozilla.com/3",
1900 view_time Some(20000),
1901 search_term None,
1902 document_type Some(DocumentType::Regular),
1903 referrer_url None,
1904 title None
1905 );
1906 let after_meta3 = Timestamp::now().as_millis() as i64;
1907
1908 delete_older_than(&conn, beginning).expect("delete worked");
1910 assert_eq!(3, get_since(&conn, beginning).expect("get worked").len());
1911
1912 delete_older_than(&conn, after_meta1).expect("delete worked");
1914 assert_eq!(2, get_since(&conn, beginning).expect("get worked").len());
1915 assert_eq!(
1916 None,
1917 get_latest_for_url(&conn, &Url::parse("http://mozilla.com/1").expect("url"))
1918 .expect("get")
1919 );
1920
1921 delete_older_than(&conn, after_meta3).expect("delete worked");
1923 assert_eq!(0, get_since(&conn, beginning).expect("get worked").len());
1924 }
1925
1926 #[test]
1927 fn test_delete_between() {
1928 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1929
1930 let beginning = Timestamp::now().as_millis() as i64;
1931 bump_clock();
1932
1933 note_observation!(&conn,
1934 url "http://mozilla.com/1",
1935 view_time Some(20000),
1936 search_term None,
1937 document_type Some(DocumentType::Regular),
1938 referrer_url None,
1939 title None
1940 );
1941
1942 bump_clock();
1943
1944 note_observation!(&conn,
1945 url "http://mozilla.com/2",
1946 view_time Some(20000),
1947 search_term None,
1948 document_type Some(DocumentType::Regular),
1949 referrer_url None,
1950 title None
1951 );
1952 let after_meta2 = Timestamp::now().as_millis() as i64;
1953
1954 bump_clock();
1955
1956 note_observation!(&conn,
1957 url "http://mozilla.com/3",
1958 view_time Some(20000),
1959 search_term None,
1960 document_type Some(DocumentType::Regular),
1961 referrer_url None,
1962 title None
1963 );
1964 let after_meta3 = Timestamp::now().as_millis() as i64;
1965
1966 delete_between(&conn, after_meta2, after_meta3).expect("delete worked");
1968 assert_eq!(2, get_since(&conn, beginning).expect("get worked").len());
1969 assert_eq!(
1970 None,
1971 get_latest_for_url(&conn, &Url::parse("http://mozilla.com/3").expect("url"))
1972 .expect("get")
1973 );
1974 }
1975
1976 #[test]
1977 fn test_metadata_deletes_do_not_affect_places() {
1978 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
1979
1980 note_observation!(
1981 &conn,
1982 url "https://www.mozilla.org/first/",
1983 view_time Some(20000),
1984 search_term None,
1985 document_type Some(DocumentType::Regular),
1986 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
1987 title None
1988 );
1989
1990 note_observation!(
1991 &conn,
1992 url "https://www.mozilla.org/",
1993 view_time Some(20000),
1994 search_term None,
1995 document_type Some(DocumentType::Regular),
1996 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
1997 title None
1998 );
1999 let after_meta_added = Timestamp::now().as_millis() as i64;
2000
2001 delete_older_than(&conn, after_meta_added).expect("delete older than worked");
2003
2004 assert_table_size!(&conn, "moz_places", 3);
2007 }
2008
2009 #[test]
2010 fn test_delete_history_also_deletes_metadata_bookmarked() {
2011 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2012 let url = Url::parse("https://www.mozilla.org/bookmarked").unwrap();
2014 let bm_guid: SyncGuid = "bookmarkAAAA".into();
2015 let bm = InsertableBookmark {
2016 parent_guid: BookmarkRootGuid::Unfiled.into(),
2017 position: BookmarkPosition::Append,
2018 date_added: None,
2019 last_modified: None,
2020 guid: Some(bm_guid.clone()),
2021 url: url.clone(),
2022 title: Some("bookmarked page".to_string()),
2023 };
2024 insert_bookmark(&conn, InsertableItem::Bookmark { b: bm }).expect("bookmark should insert");
2025 let obs = VisitObservation::new(url.clone()).with_visit_type(VisitType::Link);
2026 apply_observation(&conn, obs).expect("Should apply visit");
2027 note_observation!(
2028 &conn,
2029 url url.to_string(),
2030 view_time Some(20000),
2031 search_term None,
2032 document_type Some(DocumentType::Regular),
2033 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2034 title None
2035 );
2036
2037 assert_eq!(
2039 get_visit_count(&conn, VisitTransitionSet::empty()).unwrap(),
2040 1
2041 );
2042 let place_guid = url_to_guid(&conn, &url)
2043 .expect("is valid")
2044 .expect("should exist");
2045
2046 delete_visits_for(&conn, &place_guid).expect("should work");
2047 assert!(get_raw_bookmark(&conn, &bm_guid).unwrap().is_some());
2049 let pi = fetch_page_info(&conn, &url)
2051 .expect("should work")
2052 .expect("should exist");
2053 assert!(pi.last_visit_id.is_none());
2054 assert!(get_latest_for_url(&conn, &url)
2056 .expect("should work")
2057 .is_none());
2058 }
2059
2060 #[test]
2061 fn test_delete_history_also_deletes_metadata_not_bookmarked() {
2062 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2063 let url = Url::parse("https://www.mozilla.org/not-bookmarked").unwrap();
2065 let obs = VisitObservation::new(url.clone()).with_visit_type(VisitType::Link);
2066 apply_observation(&conn, obs).expect("Should apply visit");
2067 note_observation!(
2068 &conn,
2069 url url.to_string(),
2070 view_time Some(20000),
2071 search_term None,
2072 document_type Some(DocumentType::Regular),
2073 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2074 title None
2075 );
2076
2077 assert_eq!(
2079 get_visit_count(&conn, VisitTransitionSet::empty()).unwrap(),
2080 1
2081 );
2082 let place_guid = url_to_guid(&conn, &url)
2083 .expect("is valid")
2084 .expect("should exist");
2085
2086 delete_visits_for(&conn, &place_guid).expect("should work");
2087 assert!(fetch_page_info(&conn, &url).expect("should work").is_none());
2089 assert!(get_latest_for_url(&conn, &url)
2090 .expect("should work")
2091 .is_none());
2092 }
2093
2094 #[test]
2095 fn test_delete_history_also_deletes_metadata_no_visits() {
2096 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2097 let url = Url::parse("https://www.mozilla.org/no-visits").unwrap();
2099 note_observation!(
2100 &conn,
2101 url url.to_string(),
2102 view_time Some(20000),
2103 search_term None,
2104 document_type Some(DocumentType::Regular),
2105 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2106 title None
2107 );
2108
2109 assert_eq!(
2111 get_visit_count(&conn, VisitTransitionSet::empty()).unwrap(),
2112 0
2113 );
2114 let place_guid = url_to_guid(&conn, &url)
2115 .expect("is valid")
2116 .expect("should exist");
2117
2118 delete_visits_for(&conn, &place_guid).expect("should work");
2119 assert!(fetch_page_info(&conn, &url).expect("should work").is_none());
2121 assert!(get_latest_for_url(&conn, &url)
2122 .expect("should work")
2123 .is_none());
2124 }
2125
2126 #[test]
2127 fn test_delete_between_also_deletes_metadata() -> Result<()> {
2128 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2129
2130 let now = Timestamp::now();
2131 let url = Url::parse("https://www.mozilla.org/").unwrap();
2132 let other_url =
2133 Url::parse("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox")
2134 .unwrap();
2135 let start_timestamp = Timestamp(now.as_millis() - 1000_u64);
2136 let end_timestamp = Timestamp(now.as_millis() + 1000_u64);
2137 let observation1 = VisitObservation::new(url.clone())
2138 .with_at(start_timestamp)
2139 .with_title(Some(String::from("Test page 0")))
2140 .with_is_remote(false)
2141 .with_visit_type(VisitType::Link);
2142
2143 let observation2 = VisitObservation::new(other_url)
2144 .with_at(end_timestamp)
2145 .with_title(Some(String::from("Test page 1")))
2146 .with_is_remote(false)
2147 .with_visit_type(VisitType::Link);
2148
2149 apply_observation(&conn, observation1).expect("Should apply visit");
2150 apply_observation(&conn, observation2).expect("Should apply visit");
2151
2152 note_observation!(
2153 &conn,
2154 url "https://www.mozilla.org/",
2155 view_time Some(20000),
2156 search_term Some("mozilla firefox"),
2157 document_type Some(DocumentType::Regular),
2158 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2159 title None
2160 );
2161 assert_eq!(
2162 "https://www.mozilla.org/",
2163 get_latest_for_url(&conn, &url)?.unwrap().url
2164 );
2165 delete_visits_between(&conn, start_timestamp, end_timestamp)?;
2166 assert_eq!(None, get_latest_for_url(&conn, &url)?);
2167 Ok(())
2168 }
2169
2170 #[test]
2171 fn test_places_delete_triggers_with_bookmarks() {
2172 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2174
2175 let now = Timestamp::now();
2176 let url = Url::parse("https://www.mozilla.org/").unwrap();
2177 let parent_url =
2178 Url::parse("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox")
2179 .unwrap();
2180
2181 let observation1 = VisitObservation::new(url.clone())
2182 .with_at(now)
2183 .with_title(Some(String::from("Test page 0")))
2184 .with_is_remote(false)
2185 .with_visit_type(VisitType::Link);
2186
2187 let observation2 = VisitObservation::new(parent_url.clone())
2188 .with_at(now)
2189 .with_title(Some(String::from("Test page 1")))
2190 .with_is_remote(false)
2191 .with_visit_type(VisitType::Link);
2192
2193 apply_observation(&conn, observation1).expect("Should apply visit");
2194 apply_observation(&conn, observation2).expect("Should apply visit");
2195
2196 assert_table_size!(&conn, "moz_bookmarks", 5);
2197
2198 insert_bookmark(
2200 &conn,
2201 InsertableItem::Bookmark {
2202 b: InsertableBookmark {
2203 parent_guid: BookmarkRootGuid::Unfiled.into(),
2204 position: BookmarkPosition::Append,
2205 date_added: None,
2206 last_modified: None,
2207 guid: Some(SyncGuid::from("cccccccccccc")),
2208 url,
2209 title: None,
2210 },
2211 },
2212 )
2213 .expect("bookmark insert worked");
2214
2215 insert_bookmark(
2217 &conn,
2218 InsertableItem::Bookmark {
2219 b: InsertableBookmark {
2220 parent_guid: BookmarkRootGuid::Unfiled.into(),
2221 position: BookmarkPosition::Append,
2222 date_added: None,
2223 last_modified: None,
2224 guid: Some(SyncGuid::from("ccccccccccca")),
2225 url: parent_url,
2226 title: None,
2227 },
2228 },
2229 )
2230 .expect("bookmark insert worked");
2231
2232 assert_table_size!(&conn, "moz_bookmarks", 7);
2233 assert_table_size!(&conn, "moz_origins", 2);
2234
2235 note_observation!(
2236 &conn,
2237 url "https://www.mozilla.org/",
2238 view_time Some(20000),
2239 search_term Some("mozilla firefox"),
2240 document_type Some(DocumentType::Regular),
2241 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2242 title None
2243 );
2244
2245 assert_table_size!(&conn, "moz_origins", 2);
2246
2247 delete_everything(&conn).expect("places wipe succeeds");
2249
2250 assert_table_size!(&conn, "moz_places_metadata", 0);
2251 assert_table_size!(&conn, "moz_places_metadata_search_queries", 0);
2252 }
2253
2254 #[test]
2255 fn test_places_delete_triggers() {
2256 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2258
2259 let now = Timestamp::now();
2260 let observation1 = VisitObservation::new(Url::parse("https://www.mozilla.org/").unwrap())
2261 .with_at(now)
2262 .with_title(Some(String::from("Test page 1")))
2263 .with_is_remote(false)
2264 .with_visit_type(VisitType::Link);
2265 let observation2 =
2266 VisitObservation::new(Url::parse("https://www.mozilla.org/another/").unwrap())
2267 .with_at(Timestamp(now.as_millis() + 10000))
2268 .with_title(Some(String::from("Test page 3")))
2269 .with_is_remote(false)
2270 .with_visit_type(VisitType::Link);
2271 let observation3 =
2272 VisitObservation::new(Url::parse("https://www.mozilla.org/first/").unwrap())
2273 .with_at(Timestamp(now.as_millis() - 10000))
2274 .with_title(Some(String::from("Test page 0")))
2275 .with_is_remote(true)
2276 .with_visit_type(VisitType::Link);
2277 apply_observation(&conn, observation1).expect("Should apply visit");
2278 apply_observation(&conn, observation2).expect("Should apply visit");
2279 apply_observation(&conn, observation3).expect("Should apply visit");
2280
2281 note_observation!(
2282 &conn,
2283 url "https://www.mozilla.org/first/",
2284 view_time Some(20000),
2285 search_term None,
2286 document_type Some(DocumentType::Regular),
2287 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2288 title None
2289 );
2290
2291 note_observation!(
2292 &conn,
2293 url "https://www.mozilla.org/",
2294 view_time Some(20000),
2295 search_term None,
2296 document_type Some(DocumentType::Regular),
2297 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2298 title None
2299 );
2300
2301 note_observation!(
2302 &conn,
2303 url "https://www.mozilla.org/",
2304 view_time Some(20000),
2305 search_term Some("mozilla"),
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 note_observation!(
2312 &conn,
2313 url "https://www.mozilla.org/",
2314 view_time Some(25000),
2315 search_term Some("firefox"),
2316 document_type Some(DocumentType::Media),
2317 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2318 title None
2319 );
2320
2321 note_observation!(
2322 &conn,
2323 url "https://www.mozilla.org/another/",
2324 view_time Some(20000),
2325 search_term Some("mozilla"),
2326 document_type Some(DocumentType::Regular),
2327 referrer_url Some("https://www.google.com/search?client=firefox-b-d&q=mozilla+firefox"),
2328 title None
2329 );
2330
2331 assert!(conn
2333 .try_query_one::<i64, _>(
2334 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
2335 rusqlite::named_params! { ":term": "firefox" },
2336 true
2337 )
2338 .expect("select works")
2339 .is_some());
2340
2341 delete_visits_between(
2343 &conn,
2344 Timestamp(now.as_millis() - 1000),
2345 Timestamp(now.as_millis() + 1000),
2346 )
2347 .expect("delete worked");
2348
2349 let meta1 =
2350 get_latest_for_url(&conn, &Url::parse("https://www.mozilla.org/").expect("url"))
2351 .expect("get worked");
2352 let meta2 = get_latest_for_url(
2353 &conn,
2354 &Url::parse("https://www.mozilla.org/another/").expect("url"),
2355 )
2356 .expect("get worked");
2357
2358 assert!(meta1.is_none(), "expected metadata to have been deleted");
2359 assert!(meta2.is_none(), "expected metadata to been deleted");
2363
2364 assert!(
2366 conn.try_query_one::<i64, _>(
2367 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
2368 rusqlite::named_params! { ":term": "mozilla" },
2369 true
2370 )
2371 .expect("select works")
2372 .is_none(),
2373 "search_query records with related metadata should have been deleted"
2374 );
2375
2376 assert!(
2378 conn.try_query_one::<i64, _>(
2379 "SELECT id FROM moz_places_metadata_search_queries WHERE term = :term",
2380 rusqlite::named_params! { ":term": "firefox" },
2381 true
2382 )
2383 .expect("select works")
2384 .is_none(),
2385 "search_query records without related metadata should have been deleted"
2386 );
2387
2388 delete_everything(&conn).expect("places wipe succeeds");
2390
2391 assert_table_size!(&conn, "moz_places_metadata", 0);
2392 assert_table_size!(&conn, "moz_places_metadata_search_queries", 0);
2393 }
2394
2395 #[test]
2396 fn test_if_page_missing_behavior() {
2397 let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");
2398
2399 note_observation!(
2400 &conn,
2401 NoteHistoryMetadataObservationOptions::new()
2402 .if_page_missing(HistoryMetadataPageMissingBehavior::IgnoreObservation),
2403 url "https://www.example.com/",
2404 view_time None,
2405 search_term None,
2406 document_type Some(DocumentType::Regular),
2407 referrer_url None,
2408 title None
2409 );
2410
2411 let observations = get_since(&conn, 0).expect("should get all metadata observations");
2412 assert_eq!(observations, &[]);
2413
2414 let visit_observation =
2415 VisitObservation::new(Url::parse("https://www.example.com/").unwrap())
2416 .with_at(Timestamp::now());
2417 apply_observation(&conn, visit_observation).expect("should apply visit observation");
2418
2419 note_observation!(
2420 &conn,
2421 NoteHistoryMetadataObservationOptions::new()
2422 .if_page_missing(HistoryMetadataPageMissingBehavior::IgnoreObservation),
2423 url "https://www.example.com/",
2424 view_time None,
2425 search_term None,
2426 document_type Some(DocumentType::Regular),
2427 referrer_url None,
2428 title None
2429 );
2430
2431 let observations = get_since(&conn, 0).expect("should get all metadata observations");
2432 assert_eq!(
2433 observations
2434 .into_iter()
2435 .map(|m| m.url)
2436 .collect::<Vec<String>>(),
2437 &["https://www.example.com/"]
2438 );
2439
2440 note_observation!(
2441 &conn,
2442 NoteHistoryMetadataObservationOptions::new()
2443 .if_page_missing(HistoryMetadataPageMissingBehavior::InsertPage),
2444 url "https://www.example.org/",
2445 view_time None,
2446 search_term None,
2447 document_type Some(DocumentType::Regular),
2448 referrer_url None,
2449 title None
2450 );
2451
2452 let observations = get_since(&conn, 0).expect("should get all metadata observations");
2453 assert_eq!(
2454 observations
2455 .into_iter()
2456 .map(|m| m.url)
2457 .collect::<Vec<String>>(),
2458 &[
2459 "https://www.example.org/", "https://www.example.com/",
2461 ],
2462 );
2463 }
2464}