Bug 1271801 - Change moz_places.typed column from a boolean into a visit count; r?mak draft
authorThomas Wisniewski <wisniewskit@gmail.com>
Tue, 11 Jul 2017 00:28:04 -0400
changeset 613810 8fc83870a3f91e2b24c48273471605b698b17a29
parent 606556 0e41d07a703f19224f60b01577b2cbb5708046c9
child 638724 6a85e2f82d39ac74f483c9da22f218114e8b1785
push id69855
push userwisniewskit@gmail.com
push dateSat, 22 Jul 2017 19:00:59 +0000
reviewersmak
bugs1271801
milestone56.0a1
Bug 1271801 - Change moz_places.typed column from a boolean into a visit count; r?mak MozReview-Commit-ID: LHN4b9sLuAl
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/History.cpp
toolkit/components/places/PlacesDBUtils.jsm
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/nsINavHistoryService.idl
toolkit/components/places/nsPlacesTriggers.h
toolkit/components/places/tests/history/test_typed_counts_kept_in_sync.js
toolkit/components/places/tests/history/xpcshell.ini
toolkit/components/places/tests/migration/places_v39.sqlite
toolkit/components/places/tests/migration/test_current_from_v39.js
toolkit/components/places/tests/migration/xpcshell.ini
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -1111,16 +1111,22 @@ Database::InitSchema(bool* aDatabaseMigr
       // Firefox 55 uses schema version 37.
 
       if (currentSchemaVersion < 38) {
         rv = MigrateV38Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
       // Firefox 56 uses schema version 38.
 
+      if (currentSchemaVersion < 39) {
+        rv = MigrateV39Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+      // Firefox 57 uses schema version 39.
+
       // Schema Upgrades must add migration code here.
 
       rv = UpdateBookmarkRootTitles();
       // We don't want a broken localization to cause us to think
       // the database is corrupt and needs to be replaced.
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
@@ -1561,17 +1567,17 @@ Database::MigrateV18Up()
   ));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Update typed data.
   nsCOMPtr<mozIStorageAsyncStatement> updateTypedStmt;
   rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
     "UPDATE moz_hosts SET typed = 1 WHERE host IN ( "
       "SELECT fixup_url(get_unreversed_host(rev_host)) "
-      "FROM moz_places WHERE typed = 1 "
+      "FROM moz_places WHERE typed > 0 "
     ") "
   ), getter_AddRefs(updateTypedStmt));
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<mozIStoragePendingStatement> ps;
   rv = updateTypedStmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -2328,16 +2334,38 @@ Database::MigrateV38Up()
     ));
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   return NS_OK;
 }
 
 nsresult
+Database::MigrateV39Up()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "UPDATE moz_places SET typed = ( "
+      "SELECT COUNT(*) FROM moz_historyvisits WHERE visit_type = 2 "
+    ") "));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "UPDATE moz_hosts SET typed = 1 WHERE host IN ( "
+      "SELECT fixup_url(get_unreversed_host(rev_host)) "
+      "FROM moz_places WHERE typed > 0 "
+    ") "
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+nsresult
 Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
                            nsTArray<int64_t>& aItemIds)
 {
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT b.id FROM moz_items_annos a "
     "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
     "JOIN moz_bookmarks b ON b.id = a.item_id "
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -288,16 +288,17 @@ protected:
   nsresult MigrateV31Up();
   nsresult MigrateV32Up();
   nsresult MigrateV33Up();
   nsresult MigrateV34Up();
   nsresult MigrateV35Up();
   nsresult MigrateV36Up();
   nsresult MigrateV37Up();
   nsresult MigrateV38Up();
+  nsresult MigrateV39Up();
 
   nsresult UpdateBookmarkRootTitles();
 
   friend class ConnectionShutdownBlocker;
 
   int64_t CreateMobileRoot();
   nsresult GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
                             nsTArray<int64_t>& aItemIds);
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -67,17 +67,17 @@ namespace places {
 //// VisitData
 
 struct VisitData {
   VisitData()
   : placeId(0)
   , visitId(0)
   , hidden(true)
   , shouldUpdateHidden(true)
-  , typed(false)
+  , typed(0)
   , transitionType(UINT32_MAX)
   , visitTime(0)
   , frecency(-1)
   , lastVisitId(0)
   , lastVisitTime(0)
   , visitCount(0)
   , referrerVisitId(0)
   , titleChanged(false)
@@ -89,17 +89,17 @@ struct VisitData {
   }
 
   explicit VisitData(nsIURI* aURI,
                      nsIURI* aReferrer = nullptr)
   : placeId(0)
   , visitId(0)
   , hidden(true)
   , shouldUpdateHidden(true)
-  , typed(false)
+  , typed(0)
   , transitionType(UINT32_MAX)
   , visitTime(0)
   , frecency(-1)
   , lastVisitId(0)
   , lastVisitTime(0)
   , visitCount(0)
   , referrerVisitId(0)
   , titleChanged(false)
@@ -122,28 +122,28 @@ struct VisitData {
    * Sets the transition type of the visit, as well as if it was typed.
    *
    * @param aTransitionType
    *        The transition type constant to set.  Must be one of the
    *        TRANSITION_ constants on nsINavHistoryService.
    */
   void SetTransitionType(uint32_t aTransitionType)
   {
-    typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED;
+    MOZ_ASSERT(transitionType == UINT32_MAX, "SetTransitionTyped should only be invoked once");
     transitionType = aTransitionType;
   }
 
   int64_t placeId;
   nsCString guid;
   int64_t visitId;
   nsCString spec;
   nsString revHost;
   bool hidden;
   bool shouldUpdateHidden;
-  bool typed;
+  uint32_t typed;
   uint32_t transitionType;
   PRTime visitTime;
   int32_t frecency;
   int64_t lastVisitId;
   PRTime lastVisitTime;
   uint32_t visitCount;
 
   /**
@@ -656,17 +656,19 @@ public:
 
     // Notify the visit.  Note that TRANSITION_EMBED visits are never added
     // to the database, thus cannot be queried and we don't notify them.
     if (mPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) {
       navHistory->NotifyOnVisit(uri, mPlace.visitId, mPlace.visitTime,
                                 mPlace.referrerVisitId, mPlace.transitionType,
                                 mPlace.guid, mPlace.hidden,
                                 mPlace.visitCount + 1, // Add current visit.
-                                static_cast<uint32_t>(mPlace.typed),
+                                mPlace.typed + (mPlace.transitionType ==
+                                  nsINavHistoryService::TRANSITION_TYPED ?
+                                  1 : 0), // Add 1 to typed if visit is typed.
                                 mPlace.title);
     }
 
     nsCOMPtr<nsIObserverService> obsService =
       mozilla::services::GetObserverService();
     if (obsService) {
       DebugOnly<nsresult> rv =
         obsService->NotifyObservers(uri, URI_VISIT_SAVED, nullptr);
@@ -987,17 +989,16 @@ public:
                                       mozIStorageConnection::TRANSACTION_IMMEDIATE);
 
     VisitData* lastFetchedPlace = nullptr;
     for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
       VisitData& place = mPlaces.ElementAt(i);
 
       // Fetching from the database can overwrite this information, so save it
       // apart.
-      bool typed = place.typed;
       bool hidden = place.hidden;
 
       // We can avoid a database lookup if it's the same place as the last
       // visit we added.
       bool known = lastFetchedPlace && lastFetchedPlace->spec.Equals(place.spec);
       if (!known) {
         nsresult rv = mHistory->FetchPageInfo(place, &known);
         if (NS_FAILED(rv)) {
@@ -1013,24 +1014,20 @@ public:
         // Copy over the data from the already known place.
         place.placeId = lastFetchedPlace->placeId;
         place.guid = lastFetchedPlace->guid;
         place.lastVisitId = lastFetchedPlace->visitId;
         place.lastVisitTime = lastFetchedPlace->visitTime;
         place.titleChanged = !lastFetchedPlace->title.Equals(place.title);
         place.frecency = lastFetchedPlace->frecency;
         // Add one visit for the previous loop.
+        place.typed = ++(*lastFetchedPlace).typed;
         place.visitCount = ++(*lastFetchedPlace).visitCount;
       }
 
-      // If any transition is typed, ensure the page is marked as typed.
-      if (typed != lastFetchedPlace->typed) {
-        place.typed = true;
-      }
-
       // If any transition is visible, ensure the page is marked as visible.
       if (hidden != lastFetchedPlace->hidden) {
         place.hidden = false;
       }
 
       // If this is a new page, or the existing page was already visible,
       // there's no need to try to unhide it.
       if (!known || !lastFetchedPlace->hidden) {
@@ -2275,17 +2272,17 @@ History::FetchPageInfo(VisitData& _place
   int32_t hidden;
   rv = stmt->GetInt32(3, &hidden);
   NS_ENSURE_SUCCESS(rv, rv);
   _place.hidden = !!hidden;
 
   int32_t typed;
   rv = stmt->GetInt32(4, &typed);
   NS_ENSURE_SUCCESS(rv, rv);
-  _place.typed = !!typed;
+  _place.typed = typed;
 
   rv = stmt->GetInt32(5, &_place.frecency);
   NS_ENSURE_SUCCESS(rv, rv);
   int32_t visitCount;
   rv = stmt->GetInt32(6, &visitCount);
   NS_ENSURE_SUCCESS(rv, rv);
   _place.visitCount = visitCount;
   rv = stmt->GetInt64(7, &_place.lastVisitTime);
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -700,23 +700,27 @@ this.PlacesDBUtils = {
     };
     cleanupStatements.push(deleteUnusedKeywords);
 
     // MOZ_PLACES
     // L.2 recalculate visit_count and last_visit_date
     let fixVisitStats = {
       query:
       `UPDATE moz_places
-       SET visit_count = (SELECT count(*) FROM moz_historyvisits
+       SET typed = (SELECT count(*) FROM moz_historyvisits
+                    WHERE place_id = moz_places.id AND visit_type = 2),
+           visit_count = (SELECT count(*) FROM moz_historyvisits
                           WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7,8,9)),
            last_visit_date = (SELECT MAX(visit_date) FROM moz_historyvisits
                               WHERE place_id = moz_places.id)
        WHERE id IN (
          SELECT h.id FROM moz_places h
-         WHERE visit_count <> (SELECT count(*) FROM moz_historyvisits v
+         WHERE typed <> (SELECT count(*) FROM moz_historyvisits v
+                         WHERE v.place_id = h.id AND visit_type != 2)
+            OR visit_count <> (SELECT count(*) FROM moz_historyvisits v
                                WHERE v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9))
             OR last_visit_date <> (SELECT MAX(visit_date) FROM moz_historyvisits v
                                    WHERE v.place_id = h.id)
        )`
     };
     cleanupStatements.push(fixVisitStats);
 
     // L.3 recalculate hidden for redirects.
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -246,22 +246,22 @@ function urlQuery(conditions = "") {
           AND fixup_url(h.url) BETWEEN :searchString AND :searchString || X'FFFF'
           ${conditions}
           ORDER BY h.frecency DESC, h.id DESC
           LIMIT 1`;
 }
 
 const SQL_URL_QUERY = urlQuery();
 
-const SQL_TYPED_URL_QUERY = urlQuery("AND h.typed = 1");
+const SQL_TYPED_URL_QUERY = urlQuery("AND h.typed > 0");
 
 // TODO (bug 1045924): use foreign_count once available.
 const SQL_BOOKMARKED_URL_QUERY = urlQuery("AND bookmarked");
 
-const SQL_BOOKMARKED_TYPED_URL_QUERY = urlQuery("AND bookmarked AND h.typed = 1");
+const SQL_BOOKMARKED_TYPED_URL_QUERY = urlQuery("AND bookmarked AND h.typed > 0");
 
 // Getters
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.importGlobalProperties(["fetch"]);
 
@@ -1903,28 +1903,28 @@ Search.prototype = {
 
   /**
    * @return a string consisting of the search query to be used based on the
    * previously set urlbar suggestion preferences.
    */
   get _suggestionPrefQuery() {
     if (!this.hasBehavior("restrict") && this.hasBehavior("history") &&
         this.hasBehavior("bookmark")) {
-      return this.hasBehavior("typed") ? defaultQuery("AND h.typed = 1")
+      return this.hasBehavior("typed") ? defaultQuery("AND h.typed > 0")
                                        : defaultQuery();
     }
     let conditions = [];
     if (this.hasBehavior("history")) {
       // Enforce ignoring the visit_count index, since the frecency one is much
       // faster in this case.  ANALYZE helps the query planner to figure out the
       // faster path, but it may not have up-to-date information yet.
       conditions.push("+h.visit_count > 0");
     }
     if (this.hasBehavior("typed")) {
-      conditions.push("h.typed = 1");
+      conditions.push("h.typed > 0");
     }
     if (this.hasBehavior("bookmark")) {
       conditions.push("bookmarked");
     }
     if (this.hasBehavior("tag")) {
       conditions.push("tags NOTNULL");
     }
 
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -642,19 +642,17 @@ interface nsINavHistoryObserver : nsISup
    *        One of nsINavHistory.TRANSITION_*
    * @param aGuid
    *        The unique id associated with the page.
    * @param aHidden
    *        Whether the visited page is marked as hidden.
    * @param aVisitCount
    *        Number of visits (included this one) for this URI.
    * @param aTyped
-   *        Whether the URI has been typed or not.
-   *        TODO (Bug 1271801): This will become a count, rather than a boolean.
-   *        For future compatibility, always compare it with "> 0".
+   *        Number of times this URI was typed.
    * @param aLastKnownTitle
    *        The last known title of the page. Might not be from the current visit,
    *        and might be null if it is not known.
    */
   void onVisit(in nsIURI aURI,
                in long long aVisitId,
                in PRTime aTime,
                in long long aSessionId,
--- a/toolkit/components/places/nsPlacesTriggers.h
+++ b/toolkit/components/places/nsPlacesTriggers.h
@@ -14,37 +14,41 @@
  *  0 - invalid
  *  4 - EMBED
  *  7 - DOWNLOAD
  *  8 - FRAMED_LINK
  *  9 - RELOAD
  **/
 #define EXCLUDED_VISIT_TYPES "0, 4, 7, 8, 9"
 
+#define TYPED_VISIT_TYPE "2"
+
 /**
- * This triggers update visit_count and last_visit_date based on historyvisits
+ * This triggers update typed and visit_count and last_visit_date based on historyvisits
  * table changes.
  */
 #define CREATE_HISTORYVISITS_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_historyvisits_afterinsert_v2_trigger " \
   "AFTER INSERT ON moz_historyvisits FOR EACH ROW " \
   "BEGIN " \
     "SELECT store_last_inserted_id('moz_historyvisits', NEW.id); " \
     "UPDATE moz_places SET " \
+      "typed = typed + (SELECT NEW.visit_type = " TYPED_VISIT_TYPE "), "\
       "visit_count = visit_count + (SELECT NEW.visit_type NOT IN (" EXCLUDED_VISIT_TYPES ")), "\
       "last_visit_date = MAX(IFNULL(last_visit_date, 0), NEW.visit_date) " \
     "WHERE id = NEW.place_id;" \
   "END" \
 )
 
 #define CREATE_HISTORYVISITS_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_historyvisits_afterdelete_v2_trigger " \
   "AFTER DELETE ON moz_historyvisits FOR EACH ROW " \
   "BEGIN " \
     "UPDATE moz_places SET " \
+      "typed = typed - (SELECT OLD.visit_type = " TYPED_VISIT_TYPE "), "\
       "visit_count = visit_count - (SELECT OLD.visit_type NOT IN (" EXCLUDED_VISIT_TYPES ")), "\
       "last_visit_date = (SELECT visit_date FROM moz_historyvisits " \
                          "WHERE place_id = OLD.place_id " \
                          "ORDER BY visit_date DESC LIMIT 1) " \
     "WHERE id = OLD.place_id;" \
   "END" \
 )
 
@@ -70,46 +74,46 @@
  *       of the site.
  *  - if at least half the typed pages start with www. return www.
  *  - otherwise don't use any prefix
  */
 #define HOSTS_PREFIX_PRIORITY_FRAGMENT \
   "SELECT CASE " \
     "WHEN ( " \
       "SELECT round(avg(substr(url,1,12) = 'https://www.')) FROM moz_places h " \
-      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed > 0 " \
     ") THEN 'https://www.' " \
     "WHEN ( " \
       "SELECT round(avg(substr(url,1,8) = 'https://')) FROM moz_places h " \
-      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed > 0 " \
     ") THEN 'https://' " \
     "WHEN 1 = ( " \
       "SELECT min(substr(url,1,4) = 'ftp:') FROM moz_places h " \
-      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed > 0 " \
     ") THEN 'ftp://' " \
     "WHEN ( " \
       "SELECT round(avg(substr(url,1,11) = 'http://www.')) FROM moz_places h " \
-      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed = 1 " \
+      "WHERE (" HOST_TO_REVHOST_PREDICATE ") AND +h.typed > 0 " \
     ") THEN 'www.' " \
   "END "
 
 /**
  * These triggers update the hostnames table whenever moz_places changes.
  */
 #define CREATE_PLACES_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_places_afterinsert_trigger " \
   "AFTER INSERT ON moz_places FOR EACH ROW " \
   "BEGIN " \
     "SELECT store_last_inserted_id('moz_places', NEW.id); " \
     "INSERT OR REPLACE INTO moz_hosts (id, host, frecency, typed, prefix) " \
     "SELECT " \
         "(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
         "fixup_url(get_unreversed_host(NEW.rev_host)), " \
         "MAX(IFNULL((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), -1), NEW.frecency), " \
-        "MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed), " \
+        "MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), MIN(NEW.typed, 1)), " \
         "(" HOSTS_PREFIX_PRIORITY_FRAGMENT \
          "FROM ( " \
             "SELECT fixup_url(get_unreversed_host(NEW.rev_host)) AS host " \
           ") AS match " \
         ") " \
     " WHERE LENGTH(NEW.rev_host) > 1; " \
   "END" \
 )
@@ -174,17 +178,17 @@
                        "OR rev_host = get_unreversed_host(host || '.') || '.www.') " \
     "WHERE host = fixup_url(get_unreversed_host(NEW.rev_host)); " \
   "END" \
 )
 
 #define CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_places_afterupdate_typed_trigger " \
   "AFTER UPDATE OF typed ON moz_places FOR EACH ROW " \
-  "WHEN NEW.typed = 1 " \
+  "WHEN NEW.typed > 0 " \
   "BEGIN " \
     "UPDATE moz_hosts " \
     "SET typed = 1 " \
     "WHERE host = fixup_url(get_unreversed_host(NEW.rev_host)); " \
   "END" \
 )
 
 /**
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_typed_counts_kept_in_sync.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests for `History.insert` as implemented in History.jsm
+
+"use strict";
+
+const TEST_URL = "http://mozilla.com/";
+
+async function confirm_typed_visits(db, count) {
+  let rows = await db.execute(`SELECT IFNULL(SUM(typed),0) AS c FROM moz_places`);
+  Assert.equal(count, rows[0].getResultByName("c"), "moz_places.typed is correct");
+
+  let typed_hosts = Math.min(count, 1);
+  rows = await db.execute(`SELECT IFNULL(SUM(typed),0) AS c FROM moz_hosts`);
+  Assert.equal(typed_hosts, rows[0].getResultByName("c"), "moz_hosts.typed is correct");
+}
+
+add_task(async function test_typed_count_kept_in_sync() {
+  async function insert_typed_visit(url, date) {
+    let result = await PlacesUtils.history.insert({
+      url,
+      visits: [{
+        date,
+        transition: TRANSITION_TYPED
+      }]
+    });
+    Assert.ok(PlacesUtils.isValidGuid(result.guid), "guid for pageInfo object is valid");
+    return result.guid;
+  }
+
+  try {
+    let db = await PlacesUtils.promiseDBConnection();
+    await confirm_typed_visits(db, 0);
+
+    await insert_typed_visit(TEST_URL, new Date(1999, 0, 0, 0, 0));
+    await confirm_typed_visits(db, 1);
+
+    await insert_typed_visit(TEST_URL, new Date(2001, 0, 0, 0, 0));
+    await confirm_typed_visits(db, 2);
+
+    await PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(2001, 0, 0, 0, 0),
+                                                     endDate: new Date() });
+    await confirm_typed_visits(db, 1);
+
+    await PlacesUtils.history.removeVisitsByFilter({ beginDate: new Date(1998, 0, 0, 0, 0),
+                                                     endDate: new Date() });
+    await confirm_typed_visits(db, 0);
+  } finally {
+    await PlacesTestUtils.clearHistory();
+  }
+});
--- a/toolkit/components/places/tests/history/xpcshell.ini
+++ b/toolkit/components/places/tests/history/xpcshell.ini
@@ -5,11 +5,12 @@ head = head_history.js
 [test_fetch.js]
 [test_insert.js]
 [test_insertMany.js]
 [test_remove.js]
 [test_removeMany.js]
 [test_removeVisits.js]
 [test_removeByFilter.js]
 [test_removeVisitsByFilter.js]
+[test_typed_counts_kept_in_sync.js]
 [test_sameUri_titleChanged.js]
 [test_update.js]
 [test_updatePlaces_embed.js]
new file mode 100644
index 0000000000000000000000000000000000000000..b364c87f671b5e79a38437e8e7bdfccb98c2133c
GIT binary patch
literal 1114112
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
zAV~dW`+K?{4Si4d9q>KfV1IIU^*!^kApV7T|CPVI^5H9QzWnExpS`^0nZHbe00000
z000000000000000000000000000000006*us=a;v!O>uCJ`rpv<T^hJ|MvE8uRr|2
zTbog%_3-fhZ>^Os9t)c@)mHQ2;jKd>^(S`i9SNU0wRP-d5PakQlaGI4(+?H;gI&ve
z=`??ES?dFP&yAgW;?Sl0_w7H^ZvDU?{L)V!&i4m<g6&D;bke$Ks@0lqzJ1G<q?dZw
z8mdLJm1?y#6g4KdOg5sSQngx{jcP-!uwJT84+YOpp5L}_&#Bhvw$rnxH?BW)`Hmkr
z)4L|P+8f-nyvJH)Y^>568josQ9%+V+^I_xRN@INS2yY#qD%I;@_2CWMw%03@Q?2Ty
zS~Nb?YB#JUV}%FyAFnmbV<USuKfZDOBd6|s+tyrvaBmRI4V5d6MKi6pk2gxqsjwM5
zf8_kp;a$TA$_Hkf$AaMJe(_g|pZwKJJ<06uSb4mqayhCummF_c-+c1;mWfIuoQN)V
zD{PsY*5+2&Y=w=@tthHCmn=-P9+k_bWU|M{M-FY>HGN|9;}g3#u77^(WBZ=W_6H-u
zL*0>63nQ1q^I<ib4jZd3b|ajQs!6SK((Tssdmr69eJ(tEv=yZ{uK)QDPJR4Dra#yp
z>|EaO*vw?NyVbhwq?0paNw0@y_wSybJ-2h$)W`!#ub+SE%)8#1?hg(Jk1SuOv1&9q
zG`9+?_I#<@nwqTChbE(HyZ3PB)aav6T!@}{;OR#<uK)NeSHE~V)gO!oo0oTf;lhRM
zyaMA<z166U&9uTLgS0CL&tE!o@aU=5p40c&_6~1cKk>UCd3S$uVWs2$8N@Fo8vp<R
z00000000000000000000000000000000000cnw;Y?hW#VTq+;*oOrQv$IBl~uS+eB
zd?4N5uK4Uto+tnS000000000000000000000000000000000000NiAK?ai|{c@h8s
z000000000000000000000000000000000000C1D_Cx5;1JA(Mn;@^%x7C#?P$NwzJ
z0ssI20000000000000000000000000000000002MKgt_>bHQ*rA52$E^{_Qmi)Jg;
zYH28HOa{-d&*g%<ld@W6Y^>568joth^Kb0w)-RRIQN6je{+qJ7U}sWR4$p_xXgX{x
zExt383w9*MV>6T8=#tW6Iu|_LF0DqBL-oq!RI7SvX^q=cx!|7mbQTBul4IzN%R&67
z@n_<HAAdOhvH0Cd761SM00000000000000000000000000000000000{=a)&W;iun
zE!D%;P%WCRRI8<-s4>~QCUZ|=QGQM4?o_QZHdbj3jYqZI8!|gn<?wu1ji$rK;&$2B
zXLh8<W+uCVMFp8VGY_Z6s?p?7y)rq~s$N=Lm~Ky^R4zyL=HhTqe=@)I@BWP-{=N98
z<EP`dUir$EU%N7UW%%+-mw);4_~pBv`Crd`_?dS;Q+)UTNh$#V000000000000000
z00000000000000000000z;~!k>(>OSAXP|qbGZ$LLM9#bjJ)@q@B8S~d^&k}{M?f}
znmc!FJ2?K}ZEJ!;DwV&dTRYX4J{FFL^;YpjX|maAFm?M+cGCBJ@r6?9%;Sen9ymN(
zd-6j1p>^Gs8|GRTG6$n(tN3K4S!spkPLqd!_&2&;1o_dErykp1Ia+@3$>GE4A6naK
zni}jh&8PDD+>y?&pcfBze}KHxD)Z6{oiw}QeWj;{pK2aCG+W!(I+@<PrrY|Vxz@e;
zp1qak_)N1|Jk<Sp^I}vljvU+5?dAQCb<<D$P5sj3`R1O3qtn~=w9-G^-|c1dTratN
zc2~7pycEqeisOw^b1Li}e&(*<>87WrwvX+e89R3H{!2T@?@#Z(wcCEjT>GAUW`6V&
zVY3xBiYKC|+U+C#vtQ|^-*}<5b97?j!I_7ini+mNz5kYOACJuSk<F*)7NEFy{s-QR
zhr-r{sByN_LF$`-(e0>w<k206A8#H%cky5t9?!g^uhUUs`&>tv{Cq#VXC{j$8l~~G
zVWZo_om;!<r`MHtj8u1SJ#%EVHGMF%tI&Nl=MN{Hf3(sFC!&iBYuf((_)gQ{*FVxt
zzjkD*JhJ_<&9&LnM-Nxh`}3U>Z{cXt>GbkGj)l!+eaGjPHfTLK|3aU;xb=bY^2o%Y
z^G7H15B4T=NlvH@^GCBkIWtD;=cDTR?um8!@6UXw+r!&}gU5G`Z96k@<f;ApXVTkp
z?I{#eZ<ssx)7e@yR;h;V%BQ~i6CavSyV22(AL{A0**JF|r!$9JQ^~sTj-s=*QsZp1
z+vu5foss|U!Pn2!Cz8hHPUG~}Y`66t^CPC`FP!-%?X;D|M&~@7>9i<pnICUqpw1aH
zR%$G6-M)re-EgOM&)d_T=J~;u%@2n4nI&zLYrU4l=11<?m}<Ap=lka;yS#QIidyrv
z``b5@XK(Jk3;+NC00000000000000000000000000000000000ZnnPm=GmJ(2><{9
z00000000000000000000000000000000000xXJpHzjXX(LHx~R0{{R30000000000
z0000000000000000000000000uYHAdPjGuGsFmy4e5xn7EftK-X8MwXbo})o{@Y{&
z000000000000000000000000000000000000002rGkP<(r)uSTx+im6VQjWPiKOC}
zg5*B{00000000000000000000000000000000000004ZCSeMFdNY$d*(X-*D3sIxo
zO!p_zO#HDR{&M`OBmn>b00000000000000000000000000000000000fbVnznM|<b
zNVPN`Hn$wFl^U(xQFJy*&)%KR1S4Z}nIny8I&8EmNu_ew9B)*nTa~CjkV@LtqOnRf
z+%jLYc`S-reM!fe_?LtD%kig@1ONa400000000000000000000000000000000000
zzT*vKb`)ySSfv_n8H=K`wNm43^H>zM-jW$f%?FM&qUo^Fs)Wrg<*+&4s7$viQ9b?o
z%#PHNYH2(PA5W^acF$GM^tU^H_9h<~00000000000000000000000000000000000
z0002oWc|rsHoh;2|1`cFA4_%s00000000000000000000000000000000000007_y
z?#uQCsnp&1R4SLdEpsd!59_THrO9SC6ZDL{_nq(g;tTWXV5VL3o=(kmnS)WY^<<@4
zX@%vvs#8IJK0TAZC8>I~TQ`&5RW29DqUda`)HvHLwxVKds?sb@MAdTGXlI*Yn1sSw
zv0Q0RS4)?|axtp6BgNedtxiNyb*$9rj&=I)^M5g)-jPbCRal!(6|&jfaQ4(!Kk#h(
zuRl4qY&;yqpNW4e{^!XK000000000000000000000000000000000000002|Lu}4w
z@BP7nS~NR47DZ=krN-IjlFVqO71k>Ca(MB9V)AcGCVTHAH>y5eYJ~OHT!Zb!;fK=M
zdyl_b|J72nRg21%iAq>r7;F2sRQBGH+i%cxa;8!qovByO&4hEUhWnC3>WlwH5dU@j
z)%XwNFU0>X{zUw1@kisINrC_X000000000000000000000000000000000000B|$h
zn#rYt;2Za!eEbWW`sOna{J}5%bfKI1xnKO1;wOJK*O%!@ZCwBPm(IK^)7|~}D_6gm
z?(Y8l2d6%s?e0Fm^|5_Dg-lOj<NAr;{m8q!!9$nt_<?+P_mNX~zOA<}8MZHeEQr4y
z|8@Lw{Dt^8<9{9hSbRQyItc;*000000000000000000000000000000000000Kh-e
z+RQzvS~Nb?s*H_QYD2wm$~=;qY(zt)YPB*O)rKxrTT_#j`p{%l9UDsDp4nVjTzcWc
zg`t7W_9ZP^VZBtH9?CC|H|tTkTpH?mW9FeH@kTfuRm1aPc_{ma%+4i6opf?$Y$(%{
zS(_T_>ud|zOpwa;C3EkKKNrNW#NUd)9)B(VO8m$1i}CNrpHG4S000000000000000
z00000000000000000000008jaDVxcs`r4a9dy{W(dfS^^d(+e2WZRoedy~#&@`b*n
zQD6LtApUmz&G;|kKaIZ>|3Uou__OhUO@aUb000000000000000000000000000000
z000000Px)*mr19B=kwj9x0~d;Nl!P)c9TpuN%v&Zg(R5Cr~2BPLSNEJU;McsekJ}^
z{Pp;2@mJzMj$e#_KmL3Y1ONa40000000000000000000000000000000001h?@rlF
zkm_r13hhn4z3FXla_vn|dy{Q%GVM(|lL-oaNuzZ9CqevI$p!!b000000000000000
z000000000000000000000A9=bvxQtL=sEFX<&Kv>n7%d173Oju=u3*z@wbBbm1F||
z00000000000000000000000000000000000007@xvYC7;oyp`2eMu%Ae>sT1k!%0}
z000000000000000000000000000000000000N}Ol)=VxH^qhFHa>vUbOy82p73MM@
z=t~OM$L|T^zm5MY{`2@x;uqup9{*1K+wp&ie=GhE@yFwj#vh449RJJs$CF9`00000
z000000000000000000000000000000008h$D3?tYQhm2|lES)9l3&|Nde?N4Tz@C&
zxwVsIZ|NkNzD|-Z^kfT3&0ID}r84U~Njl$6a^0k-n`FC5rkkXDlBAICOU7RxKO4l~
zj{iD-IsQugNAdrNe=q(_{Hgdq$G;x`yZGP6ABjH{|785Wq!IuC000000000000000
z000000000000000000000Q?i_&8AYRd~R(w?OD@Jv;EyPb89zE-;&Fw3aNZwlH^nQ
zLMNB$?c_4+I(5?Nd?%V---)Jg%Vi6xR5sU1GCiFno$bjMl7`7Psgcd*^M$@-4(qSH
z62xDR|9AYw_|x%k#=jE(T>Mk<kHoDwjGv76#}CGX@f+h?l1cyo000000000000000
z000000000000000000000K6ux$)<9tzMyC1z3+U_7hkxolPz?zQ$c=RC!6nNvm4&G
zwv+AcWHX=mn>C$muA4nQ)!)hXbh7Dhyl`tLo9$-HM{en4Go5Ve)9d;=*>ooxeC<e~
zKU>J9y1BOnz5Ur>F898VPOV>(OW&I03iG*ZMGyb*Z)C3JGB3T5U&x*Qy9Zz2vyeOW
z)t~rKZc*-;b=kgT9s1&@g81e5#rU)FZ^a*re<pr^d@-I(f&c&j000000000000000
z00000000000000000000@c(9QW<%l9nS)19wf3C8zqWUHU1mdS^8B`adrq}Rx1F9n
zoo@%inNy>WK5-#>;(@0hO?LtZ_8+e`%VQ&ZHb36m4jdmJIka`x^oh-nPweh#2acRS
zI=pN6K>5II^H{DOI5fL|_w?+!ox7$+9>}x<2lk#DJN3k&OZV^Ff2Pn5jMSglwRa?Z
z>eSY;lYQ;L*7JKG-8+3QJbSbiW&4uJuD|lRAbvT1G5&1)Tk*%@pNZceUyLW?C*ytb
z{qa5V?Q#0bUtamcq!IuC000000000000000000000000000000000000N*Xzzi&TN
zpQu#B@@QL6JJ5=v>R74K31m8f(qwaVPPP-+IXW@%;LJl$%?v+%>)OnQLL-V=^EKKP
z&RyL4z<7CN;?Vh{llgX_erfW2bI-xi>1}&jYubTYG?t9oZISB)!urg77wL9jsywp&
zvCXyF(?<_i3hlt;;qh}%?r84ZvF+ga`gWkaW2Cxs>zN~?t?7e(?Let?=J7)(4;-GY
zJ$d1_PT;BGr<z9&&DQp{POfbSMo*r4Y=7lw`N1cL53g$n4j$h%w(ZQsk*D_WpXqG}
z9^G;H@#gV!7Y~Nv@mtz~?PI%V#*Q7l|I*I!`}_NnMN7wj6~up=YybcN0000000000
z0000000000000000000000000;Cn|dn+j6dbWf5LGJWmHvp0E0000000000000000
z000000000000000000000001Rll3Kk+4xIA{5SE}<F6(=00000000000000000000
s0000000000000000002+n$(+3rBb(LvYm8YCexEm6$-gdE|X6GUuym900000
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v39.js
@@ -0,0 +1,20 @@
+add_task(async function setup() {
+  await setupPlacesDatabase("places_v39.sqlite");
+});
+
+add_task(async function database_is_valid() {
+  // Accessing the database for the first time triggers migration.
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+  let db = await PlacesUtils.promiseDBConnection();
+  Assert.equal((await db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(async function test_select_new_fields() {
+  let db = await PlacesUtils.promiseDBConnection();
+  rows = await db.execute("SELECT typed FROM moz_places WHERE id=1 AND typed=2");
+  Assert.equal(1, rows.length, "moz_place found with typed=2");
+  rows = await db.execute("SELECT typed FROM moz_hosts WHERE id=1 AND typed=1");
+  Assert.equal(1, rows.length, "moz_place found with typed=1");
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -19,23 +19,25 @@ support-files =
   places_v31.sqlite
   places_v32.sqlite
   places_v33.sqlite
   places_v34.sqlite
   places_v35.sqlite
   places_v36.sqlite
   places_v37.sqlite
   places_v38.sqlite
+  places_v39.sqlite
 
 [test_current_from_downgraded.js]
 [test_current_from_v6.js]
 [test_current_from_v11.js]
 [test_current_from_v19.js]
 [test_current_from_v24.js]
 [test_current_from_v25.js]
 [test_current_from_v26.js]
 [test_current_from_v27.js]
 [test_current_from_v31.js]
 [test_current_from_v34.js]
 [test_current_from_v34_no_roots.js]
 [test_current_from_v35.js]
 [test_current_from_v36.js]
 [test_current_from_v38.js]
+[test_current_from_v39.js]