diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bbea572e..fab393147c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,9 @@ - fix `count_by_origin` and `count_by_form_action_origin` with punicode origins ### Places +- `places::storage::history_metadata::get_most_recent_search_entries()` was added to fetch the most recent search entries in history metadata. ([#7104](https://github.com/mozilla/application-services/pull/7104)) - `places::storage::history_metadata::delete_all_metadata_for_search()` was added to delete the search terms in history metadata. ([#7101](https://github.com/mozilla/application-services/pull/7101)) - # v146.0 (_2025-11-10_) ## ✨ What's New ✨ diff --git a/components/places/src/ffi.rs b/components/places/src/ffi.rs index a96f457350..39386398fa 100644 --- a/components/places/src/ffi.rs +++ b/components/places/src/ffi.rs @@ -213,6 +213,14 @@ impl PlacesConnection { self.with_conn(|conn| history_metadata::get_most_recent(conn, limit)) } + #[handle_error(crate::Error)] + pub fn get_most_recent_search_entries_in_history_metadata( + &self, + limit: i32, + ) -> ApiResult> { + self.with_conn(|conn| history_metadata::get_most_recent_search_entries(conn, limit)) + } + #[handle_error(crate::Error)] pub fn query_history_metadata( &self, diff --git a/components/places/src/places.udl b/components/places/src/places.udl index 3a9e86db92..b626ef7f4e 100644 --- a/components/places/src/places.udl +++ b/components/places/src/places.udl @@ -62,6 +62,9 @@ interface PlacesConnection { [Throws=PlacesApiError] sequence get_most_recent_history_metadata(i32 limit); + [Throws=PlacesApiError] + sequence get_most_recent_search_entries_in_history_metadata(i32 limit); + [Throws=PlacesApiError] sequence query_autocomplete(string search, i32 limit); diff --git a/components/places/src/storage/history_metadata.rs b/components/places/src/storage/history_metadata.rs index 095f2da71c..45339b9e82 100644 --- a/components/places/src/storage/history_metadata.rs +++ b/components/places/src/storage/history_metadata.rs @@ -443,6 +443,13 @@ lazy_static! { LIMIT :limit", common_select_sql = COMMON_METADATA_SELECT ); + static ref SEARCH_QUERY_SQL: String = format!( + "{common_select_sql} + WHERE search_term NOT NULL + ORDER BY updated_at DESC + LIMIT :limit", + common_select_sql = COMMON_METADATA_SELECT + ); static ref QUERY_SQL: String = format!( "{common_select_sql} WHERE @@ -507,6 +514,20 @@ pub fn get_most_recent(db: &PlacesDb, limit: i32) -> Result ) } +// Returns the most recent history metadata entries where search term is not null (newest first), +// limited by `limit`. +// +// Internally this uses [`SEARCH_QUERY_SQL`], ordered by descending `updated_at`. +pub fn get_most_recent_search_entries(db: &PlacesDb, limit: i32) -> Result> { + db.query_rows_and_then_cached( + SEARCH_QUERY_SQL.as_str(), + rusqlite::named_params! { + ":limit": limit, + }, + HistoryMetadata::from_row, + ) +} + pub fn get_highlights( db: &PlacesDb, weights: HistoryHighlightWeights, @@ -1486,6 +1507,213 @@ mod tests { assert_eq!(most_recents[2].url, "https://example.com/1"); } + #[test] + fn test_get_most_recent_search_entries_empty() { + let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db"); + let rows = get_most_recent_search_entries(&conn, 5).expect("query ok"); + assert!(rows.is_empty()); + } + + #[test] + fn test_get_most_recent_search_entries_with_limits_and_same_observation() { + let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db"); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + // Limiting to 1 should return the most recent entry where search is not null. + let most_recents1 = get_most_recent_search_entries(&conn, 1).expect("query ok"); + assert_eq!(most_recents1.len(), 1); + assert_eq!(most_recents1[0].url, "http://mozilla.org/1/"); + + // Limiting to 3 should also return one entry, since we only have one unique URL. + let most_recents2 = get_most_recent_search_entries(&conn, 3).expect("query ok"); + assert_eq!(most_recents2.len(), 1); + assert_eq!(most_recents2[0].url, "http://mozilla.org/1/"); + + // Limiting to 10 should also return one entry, since we only have one unique URL. + let most_recents3 = get_most_recent_search_entries(&conn, 10).expect("query ok"); + assert_eq!(most_recents3.len(), 1); + assert_eq!(most_recents3[0].url, "http://mozilla.org/1/"); + } + + #[test] + fn test_get_most_recent_search_entries_with_limits_and_different_observations() { + let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db"); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/2/", + view_time Some(20), + search_term None, + document_type Some(DocumentType::Regular), + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/3/", + view_time None, + search_term Some("search_term_2"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/4/", + view_time None, + search_term Some("search_term_3"), + document_type None, + referrer_url None, + title None + ); + + // Limiting to 1 should return the most recent entry where search is not null. + let most_recents1 = get_most_recent_search_entries(&conn, 1).expect("query ok"); + assert_eq!(most_recents1.len(), 1); + assert_eq!(most_recents1[0].url, "http://mozilla.org/4/"); + + // Limiting to 2 should return the two most recent entries. + let most_recents2 = get_most_recent_search_entries(&conn, 2).expect("query ok"); + assert_eq!(most_recents2.len(), 2); + assert_eq!(most_recents2[0].url, "http://mozilla.org/4/"); + assert_eq!(most_recents2[1].url, "http://mozilla.org/3/"); + + // Limiting to 10 should return all three entries, in the correct order. + let most_recents3 = get_most_recent_search_entries(&conn, 10).expect("query ok"); + assert_eq!(most_recents3.len(), 3); + assert_eq!(most_recents3[0].url, "http://mozilla.org/4/"); + assert_eq!(most_recents3[1].url, "http://mozilla.org/3/"); + assert_eq!(most_recents3[2].url, "http://mozilla.org/1/"); + } + + #[test] + fn test_get_most_recent_search_entries_with_negative_limit_with_same_observation() { + let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db"); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + // Limiting to -1 should return all entries properly ordered. + let most_recents = get_most_recent_search_entries(&conn, -1).expect("query ok"); + assert_eq!(most_recents.len(), 1); + assert_eq!(most_recents[0].url, "http://mozilla.org/1/"); + } + + #[test] + fn test_get_most_recent_search_entries_with_negative_limit_with_different_observations() { + let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db"); + + note_observation!(&conn, + url "http://mozilla.org/1/", + view_time None, + search_term Some("search_term_1"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/2/", + view_time None, + search_term Some("search_term_2"), + document_type None, + referrer_url None, + title None + ); + + bump_clock(); + + note_observation!(&conn, + url "http://mozilla.org/3/", + view_time None, + search_term Some("search_term_3"), + document_type None, + referrer_url None, + title None + ); + + // Limiting to -1 should return all entries properly ordered. + let most_recents = get_most_recent_search_entries(&conn, -1).expect("query ok"); + assert_eq!(most_recents.len(), 3); + assert_eq!(most_recents[0].url, "http://mozilla.org/3/"); + assert_eq!(most_recents[1].url, "http://mozilla.org/2/"); + assert_eq!(most_recents[2].url, "http://mozilla.org/1/"); + } + #[test] fn test_get_highlights() { let conn = PlacesDb::open_in_memory(ConnectionType::ReadWrite).expect("memory db");