Bug 1125113 - Change the keywords schema associating them with uris. r=ttaubert
authorMarco Bonardo <mbonardo@mozilla.com>
Fri, 20 Mar 2015 09:39:20 +0100
changeset 251851 687b6a735b3420cfa16cc30b55d7fd447e528fa5
parent 251850 cdec6c4e2995b280a7975754f877e83983d2af87
child 251852 296d8da2660933bf0928647f3625de83da5070bb
push id1156
push userpbrosset@mozilla.com
push dateFri, 20 Mar 2015 16:00:24 +0000
reviewersttaubert
bugs1125113
milestone39.0a1
Bug 1125113 - Change the keywords schema associating them with uris. r=ttaubert
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/PlacesDBUtils.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/nsINavBookmarksService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavBookmarks.h
toolkit/components/places/nsPlacesAutoComplete.js
toolkit/components/places/nsPlacesIndexes.h
toolkit/components/places/nsPlacesTables.h
toolkit/components/places/nsPlacesTriggers.h
toolkit/components/places/tests/autocomplete/test_keyword_search.js
toolkit/components/places/tests/bookmarks/test_keywords.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/places_v26.sqlite
toolkit/components/places/tests/migration/places_v27.sqlite
toolkit/components/places/tests/migration/test_current_from_v26.js
toolkit/components/places/tests/migration/xpcshell.ini
toolkit/components/places/tests/queries/test_sorting.js
toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
toolkit/components/places/tests/unit/test_398914.js
toolkit/components/places/tests/unit/test_async_transactions.js
toolkit/components/places/tests/unit/test_placesTxn.js
toolkit/components/places/tests/unit/test_preventive_maintenance.js
toolkit/components/places/tests/unit/test_telemetry.js
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -730,16 +730,23 @@ Database::InitSchema(bool* aDatabaseMigr
 
       if (currentSchemaVersion < 26) {
         rv = MigrateV26Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       // Firefox 37 uses schema version 26.
 
+      if (currentSchemaVersion < 27) {
+        rv = MigrateV27Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 38 uses schema version 27.
+
       // 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));
     }
   }
@@ -796,16 +803,18 @@ Database::InitSchema(bool* aDatabaseMigr
 
     // moz_bookmarks_roots.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS_ROOTS);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // moz_keywords.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_KEYWORDS);
     NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA);
+    NS_ENSURE_SUCCESS(rv, rv);
 
     // moz_favicons.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_FAVICONS);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // moz_anno_attributes.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ANNO_ATTRIBUTES);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -946,21 +955,28 @@ Database::InitTempTriggers()
   NS_ENSURE_SUCCESS(rv, rv);
   rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERDELETE_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_FRECENCY_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = mMainConn->ExecuteSimpleSQL(CREATE_FOREIGNCOUNT_AFTERDELETE_TRIGGER);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = mMainConn->ExecuteSimpleSQL(CREATE_FOREIGNCOUNT_AFTERINSERT_TRIGGER);
+
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
-  rv = mMainConn->ExecuteSimpleSQL(CREATE_FOREIGNCOUNT_AFTERUPDATE_TRIGGER);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 nsresult
 Database::UpdateBookmarkRootTitles()
 {
@@ -1482,16 +1498,76 @@ Database::MigrateV26Up() {
   nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "UPDATE moz_bookmarks SET dateAdded = dateAdded - dateAdded % 1000, "
     "                         lastModified = lastModified - lastModified % 1000"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+nsresult
+Database::MigrateV27Up() {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Change keywords store, moving their relation from bookmarks to urls.
+  nsCOMPtr<mozIStorageStatement> stmt;
+  nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT place_id FROM moz_keywords"
+  ), getter_AddRefs(stmt));
+  if (NS_FAILED(rv)) {
+    // Even if these 2 columns have a unique constraint, we allow NULL values
+    // for backwards compatibility. NULL never breaks a unique constraint.
+    rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "ALTER TABLE moz_keywords ADD COLUMN place_id INTEGER"));
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "ALTER TABLE moz_keywords ADD COLUMN post_data TEXT"));
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // Associate keywords with uris.  A keyword could be associated to multiple
+  // bookmarks uris, or multiple keywords could be associated to the same uri.
+  // The new system only allows multiple uris per keyword, provided they have
+  // a different post_data value.
+  rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "INSERT OR REPLACE INTO moz_keywords (id, keyword, place_id, post_data) "
+    "SELECT k.id, k.keyword, h.id, MAX(a.content) "
+    "FROM moz_places h "
+    "JOIN moz_bookmarks b ON b.fk = h.id "
+    "JOIN moz_keywords k ON k.id = b.keyword_id "
+    "LEFT JOIN moz_items_annos a ON a.item_id = b.id "
+    "LEFT JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id "
+                                   "AND n.name = 'bookmarkProperties/POSTData'"
+    "WHERE k.place_id ISNULL "
+    "GROUP BY keyword"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Remove any keyword that points to a non-existing place id.
+  rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_keywords "
+    "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = moz_keywords.place_id)"));
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "UPDATE moz_bookmarks SET keyword_id = NULL "
+    "WHERE NOT EXISTS (SELECT 1 FROM moz_keywords WHERE id = moz_bookmarks.keyword_id)"));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Adjust foreign_count for all the rows.
+  rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "UPDATE moz_places SET foreign_count = "
+    "(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id) + "
+    "(SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id) "
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 void
 Database::Shutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mShuttingDown);
   MOZ_ASSERT(!mClosed);
 
   mShuttingDown = true;
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -11,17 +11,17 @@
 #include "nsIObserver.h"
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 26
+#define DATABASE_SCHEMA_VERSION 27
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // Fired when initialization fails due to a locked database.
 #define TOPIC_DATABASE_LOCKED "places-database-locked"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
@@ -269,16 +269,17 @@ protected:
   nsresult MigrateV19Up();
   nsresult MigrateV20Up();
   nsresult MigrateV21Up();
   nsresult MigrateV22Up();
   nsresult MigrateV23Up();
   nsresult MigrateV24Up();
   nsresult MigrateV25Up();
   nsresult MigrateV26Up();
+  nsresult MigrateV27Up();
 
   nsresult UpdateBookmarkRootTitles();
 
 private:
   ~Database();
 
   /**
    * Singleton getter, invoked by class instantiation.
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -469,33 +469,16 @@ this.PlacesDBUtils = {
     fixOrphanItems.params["unsorted_folder"] = PlacesUtils.unfiledBookmarksFolderId;
     fixOrphanItems.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
     fixOrphanItems.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
     fixOrphanItems.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
     fixOrphanItems.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
     fixOrphanItems.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
     cleanupStatements.push(fixOrphanItems);
 
-    // D.5 fix wrong keywords
-    let fixInvalidKeywords = DBConn.createAsyncStatement(
-      `UPDATE moz_bookmarks SET keyword_id = NULL WHERE guid NOT IN (
-         :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
-       ) AND id IN (
-         SELECT id FROM moz_bookmarks b
-         WHERE keyword_id NOT NULL
-           AND NOT EXISTS
-             (SELECT id FROM moz_keywords WHERE id = b.keyword_id LIMIT 1)
-       )`);
-    fixInvalidKeywords.params["rootGuid"] = PlacesUtils.bookmarks.rootGuid;
-    fixInvalidKeywords.params["menuGuid"] = PlacesUtils.bookmarks.menuGuid;
-    fixInvalidKeywords.params["toolbarGuid"] = PlacesUtils.bookmarks.toolbarGuid;
-    fixInvalidKeywords.params["unfiledGuid"] = PlacesUtils.bookmarks.unfiledGuid;
-    fixInvalidKeywords.params["tagsGuid"] = PlacesUtils.bookmarks.tagsGuid;
-    cleanupStatements.push(fixInvalidKeywords);
-
     // D.6 fix wrong item types
     //     Folders and separators should not have an fk.
     //     If they have a valid fk convert them to bookmarks. Later in D.9 we
     //     will move eventual children to unsorted bookmarks.
     let fixBookmarksAsFolders = DBConn.createAsyncStatement(
       `UPDATE moz_bookmarks SET type = :bookmark_type WHERE guid NOT IN (
          :rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid  /* skip roots */
        ) AND id IN (
@@ -676,17 +659,17 @@ this.PlacesDBUtils = {
     cleanupStatements.push(deleteOrphanItemsAnnos);
 
     // MOZ_KEYWORDS
     // I.1 remove unused keywords
     let deleteUnusedKeywords = DBConn.createAsyncStatement(
       `DELETE FROM moz_keywords WHERE id IN (
          SELECT id FROM moz_keywords k
          WHERE NOT EXISTS
-           (SELECT id FROM moz_bookmarks WHERE keyword_id = k.id LIMIT 1)
+           (SELECT 1 FROM moz_places h WHERE k.place_id = h.id)
        )`);
     cleanupStatements.push(deleteUnusedKeywords);
 
     // MOZ_PLACES
     // L.1 fix wrong favicon ids
     let fixInvalidFaviconIds = DBConn.createAsyncStatement(
       `UPDATE moz_places SET favicon_id = NULL WHERE id IN (
          SELECT id FROM moz_places h
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -807,61 +807,81 @@ this.PlacesUtils = {
   },
 
   /**
    * Set the POST data associated with a bookmark, if any.
    * Used by POST keywords.
    *   @param aBookmarkId
    *   @returns string of POST data
    */
-  setPostDataForBookmark: function PU_setPostDataForBookmark(aBookmarkId, aPostData) {
-    const annos = this.annotations;
-    if (aPostData)
-      annos.setItemAnnotation(aBookmarkId, this.POST_DATA_ANNO, aPostData, 
-                              0, Ci.nsIAnnotationService.EXPIRE_NEVER);
-    else if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
-      annos.removeItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
+  setPostDataForBookmark(aBookmarkId, aPostData) {
+    // For now we don't have a unified API to create a keyword with postData,
+    // thus here we can just try to complete a keyword that should already exist
+    // without any post data.
+    let nullPostDataFragment = aPostData ? "AND post_data ISNULL" : "";
+    let stmt = PlacesUtils.history.DBConnection.createStatement(
+      `UPDATE moz_keywords SET post_data = :post_data
+       WHERE id = (SELECT k.id FROM moz_keywords k
+                   JOIN moz_bookmarks b ON b.fk = k.place_id
+                   WHERE b.id = :item_id
+                   ${nullPostDataFragment}
+                   LIMIT 1)`);
+    stmt.params.item_id = aBookmarkId;
+    stmt.params.post_data = aPostData;
+    try {
+      stmt.execute();
+    }
+    finally {
+      stmt.finalize();
+    }
   },
 
   /**
    * Get the POST data associated with a bookmark, if any.
    * @param aBookmarkId
    * @returns string of POST data if set for aBookmarkId. null otherwise.
    */
-  getPostDataForBookmark: function PU_getPostDataForBookmark(aBookmarkId) {
-    const annos = this.annotations;
-    if (annos.itemHasAnnotation(aBookmarkId, this.POST_DATA_ANNO))
-      return annos.getItemAnnotation(aBookmarkId, this.POST_DATA_ANNO);
-
-    return null;
+  getPostDataForBookmark(aBookmarkId) {
+    let stmt = PlacesUtils.history.DBConnection.createStatement(
+      `SELECT k.post_data
+       FROM moz_keywords k
+       JOIN moz_places h ON h.id = k.place_id
+       JOIN moz_bookmarks b ON b.fk = h.id
+       WHERE b.id = :item_id`);
+    stmt.params.item_id = aBookmarkId;
+    try {
+      if (!stmt.executeStep())
+        return null;
+      return stmt.row.post_data;
+    }
+    finally {
+      stmt.finalize();
+    }
   },
 
   /**
    * Get the URI (and any associated POST data) for a given keyword.
    * @param aKeyword string keyword
    * @returns an array containing a string URL and a string of POST data
    */
-  getURLAndPostDataForKeyword: function PU_getURLAndPostDataForKeyword(aKeyword) {
-    var url = null, postdata = null;
+  getURLAndPostDataForKeyword(aKeyword) {
+    let stmt = PlacesUtils.history.DBConnection.createStatement(
+      `SELECT h.url, k.post_data
+       FROM moz_keywords k
+       JOIN moz_places h ON h.id = k.place_id
+       WHERE k.keyword = :keyword`);
+    stmt.params.keyword = aKeyword;
     try {
-      var uri = this.bookmarks.getURIForKeyword(aKeyword);
-      if (uri) {
-        url = uri.spec;
-        var bookmarks = this.bookmarks.getBookmarkIdsForURI(uri);
-        for (let i = 0; i < bookmarks.length; i++) {
-          var bookmark = bookmarks[i];
-          var kw = this.bookmarks.getKeywordForBookmark(bookmark);
-          if (kw == aKeyword) {
-            postdata = this.getPostDataForBookmark(bookmark);
-            break;
-          }
-        }
-      }
-    } catch(ex) {}
-    return [url, postdata];
+      if (!stmt.executeStep())
+        return [ null, null ];
+      return [ stmt.row.url, stmt.row.post_data ];
+    }
+    finally {
+      stmt.finalize();
+    }
   },
 
   /**
    * Get all bookmarks for a URL, excluding items under tags.
    */
   getBookmarksForURI:
   function PU_getBookmarksForURI(aURI) {
     var bmkIds = this.bookmarks.getBookmarkIdsForURI(aURI);
@@ -3064,17 +3084,17 @@ PlacesSortFolderByNameTransaction.protot
     PlacesUtils.bookmarks.runInBatchMode(callback, null);
   },
 
   undoTransaction: function SFBNTXN_undoTransaction()
   {
     let callback = {
       _self: this,
       runBatched: function() {
-        for (item in this._self._oldOrder)
+        for (let item in this._self._oldOrder)
           PlacesUtils.bookmarks.setItemIndex(item, this._self._oldOrder[item]);
       }
     };
     PlacesUtils.bookmarks.runInBatchMode(callback, null);
   }
 };
 
 
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -147,34 +147,30 @@ const SQL_ADAPTIVE_QUERY =
                             h.visit_count, h.typed, bookmarked,
                             t.open_count,
                             :matchBehavior, :searchBehavior)
    ORDER BY rank DESC, h.frecency DESC`;
 
 const SQL_KEYWORD_QUERY =
   `/* do not warn (bug 487787) */
    SELECT :query_type,
-     (SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)
-     AS search_url, h.title,
+          REPLACE(h.url, '%s', :query_string) AS search_url, h.title,
      IFNULL(f.url, (SELECT f.url
                     FROM moz_places
                     JOIN moz_favicons f ON f.id = favicon_id
-                    WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)
+                    WHERE rev_host = h.rev_host
                     ORDER BY frecency DESC
                     LIMIT 1)
            ),
-     1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),
-     t.open_count, h.frecency
+     1, NULL, NULL, h.visit_count, h.typed, h.id, t.open_count, h.frecency
    FROM moz_keywords k
-   JOIN moz_bookmarks b ON b.keyword_id = k.id
-   LEFT JOIN moz_places h ON h.url = search_url
+   JOIN moz_places h ON k.place_id = h.id
    LEFT JOIN moz_favicons f ON f.id = h.favicon_id
    LEFT JOIN moz_openpages_temp t ON t.url = search_url
-   WHERE LOWER(k.keyword) = LOWER(:keyword)
-   ORDER BY h.frecency DESC`;
+   WHERE k.keyword = LOWER(:keyword)`;
 
 function hostQuery(conditions = "") {
   let query =
     `/* do not warn (bug NA): not worth to index on (typed, frecency) */
      SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
             NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency
      FROM moz_hosts
      WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
@@ -1236,34 +1232,23 @@ Search.prototype = {
       url = makeActionURL("switchtab", {url: escapedURL});
       action = "switchtab";
     }
 
     // Always prefer the bookmark title unless it is empty
     let title = bookmarkTitle || historyTitle;
 
     if (queryType == QUERYTYPE_KEYWORD) {
+      match.style = "keyword";
       if (this._enableActions) {
-        match.style = "keyword";
         url = makeActionURL("keyword", {
           url: escapedURL,
           input: this._originalSearchString,
         });
         action = "keyword";
-      } else {
-        // If we do not have a title, then we must have a keyword, so let the UI
-        // know it is a keyword.  Otherwise, we found an exact page match, so just
-        // show the page like a regular result.  Because the page title is likely
-        // going to be more specific than the bookmark title (keyword title).
-        if (!historyTitle) {
-          match.style = "keyword"
-        }
-        else {
-          title = historyTitle;
-        }
       }
     }
 
     // We will always prefer to show tags if we have them.
     let showTags = !!tags;
 
     // However, we'll act as if a page is not bookmarked if the user wants
     // only history and not bookmarks and there are no tags.
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -217,17 +217,17 @@ interface nsINavBookmarkObserver : nsISu
 };
 
 /**
  * The BookmarksService interface provides methods for managing bookmarked
  * history items.  Bookmarks consist of a set of user-customizable
  * folders.  A URI in history can be contained in one or more such folders.
  */
 
-[scriptable, uuid(b0f9a80a-d7f0-4421-8513-444125f0d828)]
+[scriptable, uuid(24533891-afa6-4663-b72d-3143d03f1b04)]
 interface nsINavBookmarksService : nsISupports
 {
   /**
    * The item ID of the Places root.
    */
   readonly attribute long long placesRoot;
 
   /**
@@ -532,22 +532,16 @@ interface nsINavBookmarksService : nsISu
    * Associates the given keyword with the given bookmark.
    *
    * Use an empty keyword to clear the keyword associated with the URI.
    * In both of these cases, succeeds but does nothing if the URL/keyword is not found.
    */
   void setKeywordForBookmark(in long long aItemId, in AString aKeyword);
 
   /**
-   * Retrieves the keyword for the given URI. Will be void string
-   * (null in JS) if no such keyword is found.
-   */
-  AString getKeywordForURI(in nsIURI aURI);
-
-  /**
    * Retrieves the keyword for the given bookmark. Will be void string
    * (null in JS) if no such keyword is found.
    */
   AString getKeywordForBookmark(in long long aItemId);
 
   /**
    * Returns the URI associated with the given keyword. Empty if no such
    * keyword is found.
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -14,18 +14,16 @@
 #include "nsNetUtil.h"
 #include "nsUnicharUtils.h"
 #include "nsPrintfCString.h"
 #include "prprf.h"
 #include "mozilla/storage.h"
 
 #include "GeckoProfiler.h"
 
-#define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH 32
-
 using namespace mozilla;
 
 // These columns sit to the right of the kGetInfoIndex_* columns.
 const int32_t nsNavBookmarks::kGetChildrenIndex_Guid = 15;
 const int32_t nsNavBookmarks::kGetChildrenIndex_Position = 16;
 const int32_t nsNavBookmarks::kGetChildrenIndex_Type = 17;
 const int32_t nsNavBookmarks::kGetChildrenIndex_PlaceID = 18;
 
@@ -35,35 +33,16 @@ PLACES_FACTORY_SINGLETON_IMPLEMENTATION(
 
 #define BOOKMARKS_ANNO_PREFIX "bookmarks/"
 #define BOOKMARKS_TOOLBAR_FOLDER_ANNO NS_LITERAL_CSTRING(BOOKMARKS_ANNO_PREFIX "toolbarFolder")
 #define FEED_URI_ANNO NS_LITERAL_CSTRING("livemark/feedURI")
 
 
 namespace {
 
-struct keywordSearchData
-{
-  int64_t itemId;
-  nsString keyword;
-};
-
-PLDHashOperator
-SearchBookmarkForKeyword(nsTrimInt64HashKey::KeyType aKey,
-                         const nsString aValue,
-                         void* aUserArg)
-{
-  keywordSearchData* data = reinterpret_cast<keywordSearchData*>(aUserArg);
-  if (data->keyword.Equals(aValue)) {
-    data->itemId = aKey;
-    return PL_DHASH_STOP;
-  }
-  return PL_DHASH_NEXT;
-}
-
 template<typename Method, typename DataType>
 class AsyncGetBookmarksForURI : public AsyncStatementCallback
 {
 public:
   AsyncGetBookmarksForURI(nsNavBookmarks* aBookmarksSvc,
                           Method aCallback,
                           const DataType& aData)
   : mBookmarksSvc(aBookmarksSvc)
@@ -138,18 +117,16 @@ nsNavBookmarks::nsNavBookmarks()
   , mRoot(0)
   , mMenuRoot(0)
   , mTagsRoot(0)
   , mUnfiledRoot(0)
   , mToolbarRoot(0)
   , mCanNotify(false)
   , mCacheObservers("bookmark-observers")
   , mBatching(false)
-  , mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH)
-  , mBookmarkToKeywordHashInitialized(false)
 {
   NS_ASSERTION(!gBookmarksService,
                "Attempting to create two instances of the service!");
   gBookmarksService = this;
 }
 
 
 nsNavBookmarks::~nsNavBookmarks()
@@ -641,21 +618,22 @@ nsNavBookmarks::RemoveItem(int64_t aItem
     // If not a tag, recalculate frecency for this entry, since it changed.
     if (bookmark.grandParentId != mTagsRoot) {
       nsNavHistory* history = nsNavHistory::GetHistoryService();
       NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
       rv = history->UpdateFrecency(bookmark.placeId);
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
-    rv = UpdateKeywordsHashForRemovedBookmark(aItemId);
-    NS_ENSURE_SUCCESS(rv, rv);
-
     // A broken url should not interrupt the removal process.
-    (void)NS_NewURI(getter_AddRefs(uri), bookmark.url);
+    rv = NS_NewURI(getter_AddRefs(uri), bookmark.url);
+    if (NS_SUCCEEDED(rv)) {
+      rv = UpdateKeywordsForRemovedBookmark(bookmark);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
   }
 
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavBookmarkObserver,
                    OnItemRemoved(bookmark.id,
                                  bookmark.parentId,
                                  bookmark.position,
                                  bookmark.type,
@@ -1103,42 +1081,39 @@ nsNavBookmarks::RemoveFolderChildren(int
         "LEFT JOIN moz_bookmarks b ON a.item_id = b.id "
         "WHERE b.id ISNULL)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Set the lastModified date.
   rv = SetItemDateInternal(LAST_MODIFIED, folder.id, RoundedPRNow());
   NS_ENSURE_SUCCESS(rv, rv);
 
-  for (uint32_t i = 0; i < folderChildrenArray.Length(); i++) {
+  rv = transaction.Commit();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Call observers in reverse order to serve children before their parent.
+  for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
     BookmarkData& child = folderChildrenArray[i];
+
+    nsCOMPtr<nsIURI> uri;
     if (child.type == TYPE_BOOKMARK) {
       // If not a tag, recalculate frecency for this entry, since it changed.
       if (child.grandParentId != mTagsRoot) {
         nsNavHistory* history = nsNavHistory::GetHistoryService();
         NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
         rv = history->UpdateFrecency(child.placeId);
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
-      rv = UpdateKeywordsHashForRemovedBookmark(child.id);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-  }
-
-  rv = transaction.Commit();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // Call observers in reverse order to serve children before their parent.
-  for (int32_t i = folderChildrenArray.Length() - 1; i >= 0; --i) {
-    BookmarkData& child = folderChildrenArray[i];
-    nsCOMPtr<nsIURI> uri;
-    if (child.type == TYPE_BOOKMARK) {
       // A broken url should not interrupt the removal process.
-      (void)NS_NewURI(getter_AddRefs(uri), child.url);
+      rv = NS_NewURI(getter_AddRefs(uri), child.url);
+      if (NS_SUCCEEDED(rv)) {
+        rv = UpdateKeywordsForRemovedBookmark(child);
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
     }
 
     NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                      nsINavBookmarkObserver,
                      OnItemRemoved(child.id,
                                    child.parentId,
                                    child.position,
                                    child.type,
@@ -2259,198 +2234,287 @@ nsNavBookmarks::SetItemIndex(int64_t aIt
                                bookmark.parentGuid,
                                bookmark.parentGuid));
 
   return NS_OK;
 }
 
 
 nsresult
-nsNavBookmarks::UpdateKeywordsHashForRemovedBookmark(int64_t aItemId)
+nsNavBookmarks::UpdateKeywordsForRemovedBookmark(const BookmarkData& aBookmark)
 {
-  nsAutoString keyword;
-  if (NS_SUCCEEDED(GetKeywordForBookmark(aItemId, keyword)) &&
-      !keyword.IsEmpty()) {
-    nsresult rv = EnsureKeywordsHash();
+  // If there are no keywords for this URI, there's nothing to do.
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = NS_NewURI(getter_AddRefs(uri), aBookmark.url);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsTArray<nsString> keywords;
+  {
+    nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+      "SELECT keyword FROM moz_keywords WHERE place_id = :place_id "
+    );
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+    rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), aBookmark.placeId);
     NS_ENSURE_SUCCESS(rv, rv);
-    mBookmarkToKeywordHash.Remove(aItemId);
-
-    // If the keyword is unused, remove it from the database.
-    keywordSearchData searchData;
-    searchData.keyword.Assign(keyword);
-    searchData.itemId = -1;
-    mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData);
-    if (searchData.itemId == -1) {
-      nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
-        "DELETE FROM moz_keywords "
-        "WHERE keyword = :keyword "
-        "AND NOT EXISTS ( "
-          "SELECT id "
-          "FROM moz_bookmarks "
-          "WHERE keyword_id = moz_keywords.id "
-        ")"
+
+    bool hasMore;
+    while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+      nsAutoString keyword;
+      rv = stmt->GetString(0, keyword);
+      NS_ENSURE_SUCCESS(rv, rv);
+      keywords.AppendElement(keyword);
+    }
+  }
+
+  if (keywords.Length() == 0) {
+    // This uri has no keywords associated, so there's nothing to do.
+    return NS_OK;
+  }
+
+  // If the uri is not bookmarked anymore, we can remove its keywords.
+  nsTArray<BookmarkData> bookmarks;
+  rv = GetBookmarksForURI(uri, bookmarks);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (bookmarks.Length() == 0) {
+    for (uint32_t i = 0; i < keywords.Length(); ++i) {
+      nsString keyword = keywords[i];
+
+      nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+        "DELETE FROM moz_keywords WHERE keyword = :keyword "
       );
       NS_ENSURE_STATE(stmt);
-
+      mozStorageStatementScoper scoper(stmt);
       rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
       NS_ENSURE_SUCCESS(rv, rv);
-      nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
-      rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt));
+      rv = stmt->Execute();
       NS_ENSURE_SUCCESS(rv, rv);
     }
+
+    NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                     nsINavBookmarkObserver,
+                     OnItemChanged(aBookmark.id,
+                                   NS_LITERAL_CSTRING("keyword"),
+                                   false,
+                                   EmptyCString(),
+                                   aBookmark.lastModified,
+                                   TYPE_BOOKMARK,
+                                   aBookmark.parentId,
+                                   aBookmark.guid,
+                                   aBookmark.parentGuid));
   }
+
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
                                       const nsAString& aUserCasedKeyword)
 {
   NS_ENSURE_ARG_MIN(aBookmarkId, 1);
 
   // This also ensures the bookmark is valid.
   BookmarkData bookmark;
   nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
   NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = EnsureKeywordsHash();
+  nsCOMPtr<nsIURI> uri;
+  rv = NS_NewURI(getter_AddRefs(uri), bookmark.url);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Shortcuts are always lowercased internally.
   nsAutoString keyword(aUserCasedKeyword);
   ToLowerCase(keyword);
 
-  // Check if bookmark was already associated to a keyword.
-  nsAutoString oldKeyword;
-  rv = GetKeywordForBookmark(bookmark.id, oldKeyword);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // Trying to set the same value or to remove a nonexistent keyword is a no-op.
-  if (keyword.Equals(oldKeyword) || (keyword.IsEmpty() && oldKeyword.IsEmpty()))
+  // The same URI can be associated to more than one keyword, provided the post
+  // data differs.  Check if there are already keywords associated to this uri.
+  nsTArray<nsString> oldKeywords;
+  {
+    nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+      "SELECT keyword FROM moz_keywords WHERE place_id = :place_id"
+    );
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+    rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    bool hasMore;
+    while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+      nsString oldKeyword;
+      rv = stmt->GetString(0, oldKeyword);
+      NS_ENSURE_SUCCESS(rv, rv);
+      oldKeywords.AppendElement(oldKeyword);
+    }
+  }
+
+  // Trying to remove a non-existent keyword is a no-op.
+  if (keyword.IsEmpty() && oldKeywords.Length() == 0) {
     return NS_OK;
-
-  mozStorageTransaction transaction(mDB->MainConn(), false);
-
-  nsCOMPtr<mozIStorageStatement> updateBookmarkStmt = mDB->GetStatement(
-    "UPDATE moz_bookmarks "
-    "SET keyword_id = (SELECT id FROM moz_keywords WHERE keyword = :keyword), "
-        "lastModified = :date "
-    "WHERE id = :item_id "
-  );
-  NS_ENSURE_STATE(updateBookmarkStmt);
-  mozStorageStatementScoper updateBookmarkScoper(updateBookmarkStmt);
+  }
 
   if (keyword.IsEmpty()) {
-    // Remove keyword association from the hash.
-    mBookmarkToKeywordHash.Remove(bookmark.id);
-    rv = updateBookmarkStmt->BindNullByName(NS_LITERAL_CSTRING("keyword"));
-  }
-   else {
-    // We are associating bookmark to a new keyword. Create a new keyword
-    // record if needed.
-    nsCOMPtr<mozIStorageStatement> newKeywordStmt = mDB->GetStatement(
-      "INSERT OR IGNORE INTO moz_keywords (keyword) VALUES (:keyword)"
-    );
-    NS_ENSURE_STATE(newKeywordStmt);
-    mozStorageStatementScoper newKeywordScoper(newKeywordStmt);
-
-    rv = newKeywordStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"),
-                                          keyword);
+    // We are removing the existing keywords.
+    for (uint32_t i = 0; i < oldKeywords.Length(); ++i) {
+      nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+        "DELETE FROM moz_keywords WHERE keyword = :old_keyword"
+      );
+      NS_ENSURE_STATE(stmt);
+      mozStorageStatementScoper scoper(stmt);
+      rv = stmt->BindStringByName(NS_LITERAL_CSTRING("old_keyword"),
+                                  oldKeywords[i]);
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = stmt->Execute();
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+
+    nsTArray<BookmarkData> bookmarks;
+    rv = GetBookmarksForURI(uri, bookmarks);
     NS_ENSURE_SUCCESS(rv, rv);
-    rv = newKeywordStmt->Execute();
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // Add new keyword association to the hash, removing the old one if needed.
-    if (!oldKeyword.IsEmpty())
-      mBookmarkToKeywordHash.Remove(bookmark.id);
-    mBookmarkToKeywordHash.Put(bookmark.id, keyword);
-    rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
+    for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+      NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                       nsINavBookmarkObserver,
+                       OnItemChanged(bookmarks[i].id,
+                                     NS_LITERAL_CSTRING("keyword"),
+                                     false,
+                                     EmptyCString(),
+                                     bookmarks[i].lastModified,
+                                     TYPE_BOOKMARK,
+                                     bookmarks[i].parentId,
+                                     bookmarks[i].guid,
+                                     bookmarks[i].parentGuid));
+    }
+
+    return NS_OK;
   }
-  NS_ENSURE_SUCCESS(rv, rv);
-  bookmark.lastModified = RoundedPRNow();
-  rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("date"),
-                                           bookmark.lastModified);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = updateBookmarkStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
-                                           bookmark.id);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = updateBookmarkStmt->Execute();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = transaction.Commit();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
-                   nsINavBookmarkObserver,
-                   OnItemChanged(bookmark.id,
-                                 NS_LITERAL_CSTRING("keyword"),
-                                 false,
-                                 NS_ConvertUTF16toUTF8(keyword),
-                                 bookmark.lastModified,
-                                 bookmark.type,
-                                 bookmark.parentId,
-                                 bookmark.guid,
-                                 bookmark.parentGuid));
-
-  return NS_OK;
-}
-
-
-NS_IMETHODIMP
-nsNavBookmarks::GetKeywordForURI(nsIURI* aURI, nsAString& aKeyword)
-{
-  PLACES_WARN_DEPRECATED();
-
-  NS_ENSURE_ARG(aURI);
-  aKeyword.Truncate(0);
-
-  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
-    "SELECT k.keyword "
-    "FROM moz_places h "
-    "JOIN moz_bookmarks b ON b.fk = h.id "
-    "JOIN moz_keywords k ON k.id = b.keyword_id "
-    "WHERE h.url = :page_url "
-  );
+
+  // A keyword can only be associated to a single URI.  Check if the requested
+  // keyword was already associated, in such a case we will need to notify about
+  // the change.
+  nsCOMPtr<nsIURI> oldUri;
+  {
+    nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
+      "SELECT url "
+      "FROM moz_keywords "
+      "JOIN moz_places h ON h.id = place_id "
+      "WHERE keyword = :keyword"
+    );
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+    rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    bool hasMore;
+    if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+      nsAutoCString spec;
+      rv = stmt->GetUTF8String(0, spec);
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = NS_NewURI(getter_AddRefs(oldUri), spec);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
+
+  // If another uri is using the new keyword, we must update the keyword entry.
+  // Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
+  // trigger.
+  nsCOMPtr<mozIStorageStatement> stmt;
+  if (oldUri) {
+    // In both cases, notify about the change.
+    nsTArray<BookmarkData> bookmarks;
+    rv = GetBookmarksForURI(oldUri, bookmarks);
+    NS_ENSURE_SUCCESS(rv, rv);
+    for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+      NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                       nsINavBookmarkObserver,
+                       OnItemChanged(bookmarks[i].id,
+                                     NS_LITERAL_CSTRING("keyword"),
+                                     false,
+                                     EmptyCString(),
+                                     bookmarks[i].lastModified,
+                                     TYPE_BOOKMARK,
+                                     bookmarks[i].parentId,
+                                     bookmarks[i].guid,
+                                     bookmarks[i].parentGuid));
+    }
+
+    stmt = mDB->GetStatement(
+      "UPDATE moz_keywords SET place_id = :place_id WHERE keyword = :keyword"
+    );
+    NS_ENSURE_STATE(stmt);
+  }
+  else {
+    stmt = mDB->GetStatement(
+      "INSERT INTO moz_keywords (keyword, place_id) "
+      "VALUES (:keyword, :place_id)"
+    );
+  }
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
-  nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("place_id"), bookmark.placeId);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = stmt->Execute();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // In both cases, notify about the change.
+  nsTArray<BookmarkData> bookmarks;
+  rv = GetBookmarksForURI(uri, bookmarks);
   NS_ENSURE_SUCCESS(rv, rv);
-
-  bool hasMore = false;
-  rv = stmt->ExecuteStep(&hasMore);
-  if (NS_FAILED(rv) || !hasMore) {
-    aKeyword.SetIsVoid(true);
-    return NS_OK; // not found: return void keyword string
+  for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
+    NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
+                     nsINavBookmarkObserver,
+                     OnItemChanged(bookmarks[i].id,
+                                   NS_LITERAL_CSTRING("keyword"),
+                                   false,
+                                   NS_ConvertUTF16toUTF8(keyword),
+                                   bookmarks[i].lastModified,
+                                   TYPE_BOOKMARK,
+                                   bookmarks[i].parentId,
+                                   bookmarks[i].guid,
+                                   bookmarks[i].parentGuid));
   }
 
-  // found, get the keyword
-  rv = stmt->GetString(0, aKeyword);
-  NS_ENSURE_SUCCESS(rv, rv);
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword)
 {
   NS_ENSURE_ARG_MIN(aBookmarkId, 1);
   aKeyword.Truncate(0);
 
-  nsresult rv = EnsureKeywordsHash();
+  // We can have multiple keywords for the same uri, here we'll just return the
+  // last created one.
+  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+    "SELECT k.keyword "
+    "FROM moz_bookmarks b "
+    "JOIN moz_keywords k ON k.place_id = b.fk "
+    "WHERE b.id = :item_id "
+    "ORDER BY k.ROWID DESC "
+    "LIMIT 1"
+  ));
+  NS_ENSURE_STATE(stmt);
+  mozStorageStatementScoper scoper(stmt);
+
+  nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+                             aBookmarkId);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsAutoString keyword;
-  if (!mBookmarkToKeywordHash.Get(aBookmarkId, &keyword)) {
-    aKeyword.SetIsVoid(true);
+  bool hasMore;
+  if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+    nsAutoString keyword;
+    rv = stmt->GetString(0, keyword);
+    NS_ENSURE_SUCCESS(rv, rv);
+    aKeyword = keyword;
+    return NS_OK;
   }
-  else {
-    aKeyword.Assign(keyword);
-  }
+
+  aKeyword.SetIsVoid(true);
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword,
                                  nsIURI** aURI)
@@ -2458,61 +2522,37 @@ nsNavBookmarks::GetURIForKeyword(const n
   NS_ENSURE_ARG_POINTER(aURI);
   NS_ENSURE_TRUE(!aUserCasedKeyword.IsEmpty(), NS_ERROR_INVALID_ARG);
   *aURI = nullptr;
 
   // Shortcuts are always lowercased internally.
   nsAutoString keyword(aUserCasedKeyword);
   ToLowerCase(keyword);
 
-  nsresult rv = EnsureKeywordsHash();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  keywordSearchData searchData;
-  searchData.keyword.Assign(keyword);
-  searchData.itemId = -1;
-  mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData);
-
-  if (searchData.itemId == -1) {
-    // Not found.
-    return NS_OK;
-  }
-
-  rv = GetBookmarkURI(searchData.itemId, aURI);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  return NS_OK;
-}
-
-
-nsresult
-nsNavBookmarks::EnsureKeywordsHash() {
-  if (mBookmarkToKeywordHashInitialized) {
-    return NS_OK;
-  }
-  mBookmarkToKeywordHashInitialized = true;
-
-  nsCOMPtr<mozIStorageStatement> stmt;
-  nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
-    "SELECT b.id, k.keyword "
-    "FROM moz_bookmarks b "
-    "JOIN moz_keywords k ON k.id = b.keyword_id "
-  ), getter_AddRefs(stmt));
+  nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(NS_LITERAL_CSTRING(
+    "SELECT h.url "
+    "FROM moz_places h "
+    "JOIN moz_keywords k ON k.place_id = h.id "
+    "WHERE k.keyword = :keyword"
+  ));
+  NS_ENSURE_STATE(stmt);
+  mozStorageStatementScoper scoper(stmt);
+
+  nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool hasMore;
-  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
-    int64_t itemId;
-    rv = stmt->GetInt64(0, &itemId);
+  if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+    nsAutoCString spec;
+    rv = stmt->GetUTF8String(0, spec);
     NS_ENSURE_SUCCESS(rv, rv);
-    nsAutoString keyword;
-    rv = stmt->GetString(1, keyword);
+    nsCOMPtr<nsIURI> uri;
+    rv = NS_NewURI(getter_AddRefs(uri), spec);
     NS_ENSURE_SUCCESS(rv, rv);
-
-    mBookmarkToKeywordHash.Put(itemId, keyword);
+    uri.forget(aURI);
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::RunInBatchMode(nsINavHistoryBatchCallback* aCallback,
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -417,25 +417,17 @@ private:
   bool mCanNotify;
   nsCategoryCache<nsINavBookmarkObserver> mCacheObservers;
 
   // Tracks whether we are in batch mode.
   // Note: this is only tracking bookmarks batches, not history ones.
   bool mBatching;
 
   /**
-   * Always call EnsureKeywordsHash() and check it for errors before actually
-   * using the hash.  Internal keyword methods are already doing that.
-   */
-  nsresult EnsureKeywordsHash();
-  nsDataHashtable<nsTrimInt64HashKey, nsString> mBookmarkToKeywordHash;
-  bool mBookmarkToKeywordHashInitialized;
-
-  /**
    * This function must be called every time a bookmark is removed.
    *
    * @param aURI
    *        Uri to test.
    */
-  nsresult UpdateKeywordsHashForRemovedBookmark(int64_t aItemId);
+  nsresult UpdateKeywordsForRemovedBookmark(const BookmarkData& aBookmark);
 };
 
 #endif // nsNavBookmarks_h_
--- a/toolkit/components/places/nsPlacesAutoComplete.js
+++ b/toolkit/components/places/nsPlacesAutoComplete.js
@@ -409,34 +409,30 @@ function nsPlacesAutoComplete()
                                 :matchBehavior, :searchBehavior)
        ORDER BY rank DESC, h.frecency DESC`
     );
   });
 
   XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() {
     return this._db.createAsyncStatement(
       `/* do not warn (bug 487787) */
-       SELECT
-       (SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)
-       AS search_url, h.title,
+       SELECT REPLACE(h.url, '%s', :query_string) AS search_url, h.title,
        IFNULL(f.url, (SELECT f.url
                       FROM moz_places
                       JOIN moz_favicons f ON f.id = favicon_id
-                      WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)
+                      WHERE rev_host = h.rev_host
                       ORDER BY frecency DESC
                       LIMIT 1)
-       ), 1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),
+       ), 1, NULL, NULL, h.visit_count, h.typed, h.id,
        :query_type, t.open_count
        FROM moz_keywords k
-       JOIN moz_bookmarks b ON b.keyword_id = k.id
-       LEFT JOIN moz_places h ON h.url = search_url
+       JOIN moz_places h ON k.place_id = h.id
        LEFT JOIN moz_favicons f ON f.id = h.favicon_id
        LEFT JOIN moz_openpages_temp t ON t.url = search_url
-       WHERE LOWER(k.keyword) = LOWER(:keyword)
-       ORDER BY h.frecency DESC`
+       WHERE k.keyword = LOWER(:keyword)`
     );
   });
 
   this._registerOpenPageQuerySQL =
     `INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)
        VALUES (:page_url,
          IFNULL(
            (
--- a/toolkit/components/places/nsPlacesIndexes.h
+++ b/toolkit/components/places/nsPlacesIndexes.h
@@ -116,9 +116,18 @@
  * moz_favicons
  */
 
 #define CREATE_IDX_MOZ_FAVICONS_GUID \
   CREATE_PLACES_IDX( \
     "guid_uniqueindex", "moz_favicons", "guid", "UNIQUE" \
   )
 
+/**
+ * moz_keywords
+ */
+
+#define CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA \
+  CREATE_PLACES_IDX( \
+    "placepostdata_uniqueindex", "moz_keywords", "place_id, post_data", "UNIQUE" \
+  )
+
 #endif // nsPlacesIndexes_h__
--- a/toolkit/components/places/nsPlacesTables.h
+++ b/toolkit/components/places/nsPlacesTables.h
@@ -116,16 +116,18 @@
     ", folder_id INTEGER" \
   ")" \
 )
 
 #define CREATE_MOZ_KEYWORDS NS_LITERAL_CSTRING( \
   "CREATE TABLE moz_keywords (" \
     "  id INTEGER PRIMARY KEY AUTOINCREMENT" \
     ", keyword TEXT UNIQUE" \
+    ", place_id INTEGER" \
+    ", post_data TEXT" \
   ")" \
 )
 
 #define CREATE_MOZ_HOSTS NS_LITERAL_CSTRING( \
   "CREATE TABLE moz_hosts (" \
     "  id INTEGER PRIMARY KEY" \
     ", host TEXT NOT NULL UNIQUE" \
     ", frecency INTEGER" \
--- a/toolkit/components/places/nsPlacesTriggers.h
+++ b/toolkit/components/places/nsPlacesTriggers.h
@@ -170,41 +170,75 @@
   "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW " \
   "WHEN NEW.open_count = 0 " \
   "BEGIN " \
     "DELETE FROM moz_openpages_temp " \
     "WHERE url = NEW.url;" \
   "END" \
 )
 
-#define CREATE_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterdelete_trigger " \
   "AFTER DELETE ON moz_bookmarks FOR EACH ROW " \
   "BEGIN " \
     "UPDATE moz_places " \
     "SET foreign_count = foreign_count - 1 " \
     "WHERE id = OLD.fk;" \
   "END" \
 )
 
-#define CREATE_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterinsert_trigger " \
   "AFTER INSERT ON moz_bookmarks FOR EACH ROW " \
   "BEGIN " \
     "UPDATE moz_places " \
     "SET foreign_count = foreign_count + 1 " \
     "WHERE id = NEW.fk;" \
   "END" \
 )
 
-#define CREATE_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
+#define CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_bookmarks_foreign_count_afterupdate_trigger " \
   "AFTER UPDATE OF fk ON moz_bookmarks FOR EACH ROW " \
   "BEGIN " \
     "UPDATE moz_places " \
     "SET foreign_count = foreign_count + 1 " \
     "WHERE id = NEW.fk;" \
     "UPDATE moz_places " \
     "SET foreign_count = foreign_count - 1 " \
     "WHERE id = OLD.fk;" \
   "END" \
 )
+
+#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMP TRIGGER moz_keywords_foreign_count_afterdelete_trigger " \
+  "AFTER DELETE ON moz_keywords FOR EACH ROW " \
+  "BEGIN " \
+    "UPDATE moz_places " \
+    "SET foreign_count = foreign_count - 1 " \
+    "WHERE id = OLD.place_id;" \
+  "END" \
+)
+
+#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMP TRIGGER moz_keyords_foreign_count_afterinsert_trigger " \
+  "AFTER INSERT ON moz_keywords FOR EACH ROW " \
+  "BEGIN " \
+    "UPDATE moz_places " \
+    "SET foreign_count = foreign_count + 1 " \
+    "WHERE id = NEW.place_id;" \
+  "END" \
+)
+
+#define CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMP TRIGGER moz_keywords_foreign_count_afterupdate_trigger " \
+  "AFTER UPDATE OF place_id ON moz_keywords FOR EACH ROW " \
+  "BEGIN " \
+    "UPDATE moz_places " \
+    "SET foreign_count = foreign_count + 1 " \
+    "WHERE id = NEW.place_id; " \
+    "UPDATE moz_places " \
+    "SET foreign_count = foreign_count - 1 " \
+    "WHERE id = OLD.place_id; " \
+  "END" \
+)
+
 #endif // __nsPlacesTriggers_h__
--- a/toolkit/components/places/tests/autocomplete/test_keyword_search.js
+++ b/toolkit/components/places/tests/autocomplete/test_keyword_search.js
@@ -38,23 +38,23 @@ let kURIs = [
 let kTitles = [
   "Generic page title",
   "Keyword title",
 ];
 
 // Add the keyword bookmark
 addPageBook(0, 0, 1, [], keyKey);
 // Add in the "fake pages" for keyword searches
-gPages[1] = [1,1];
-gPages[2] = [2,1];
-gPages[3] = [3,1];
-gPages[4] = [4,1];
+gPages[1] = [1,0];
+gPages[2] = [2,0];
+gPages[3] = [3,0];
+gPages[4] = [4,0];
 // Add a page into history
 addPageBook(5, 0);
-gPages[6] = [6,1];
+gPages[6] = [6,0];
 
 // Provide for each test: description; search terms; array of gPages indices of
 // pages that should match; optional function to be run before the test
 let gTests = [
   ["0: Plain keyword query",
    keyKey + " term", [1]],
   ["1: Multi-word keyword query",
    keyKey + " multi word", [2]],
@@ -63,19 +63,9 @@ let gTests = [
   ["3: Unescaped term in query",
    keyKey + " " + unescaped, [4]],
   ["4: Keyword that happens to match a page",
    keyKey + " " + pageInHistory, [5]],
   ["5: Keyword without query (without space)",
    keyKey, [6]],
   ["6: Keyword without query (with space)",
    keyKey + " ", [6]],
-
-  // This adds a second keyword so anything after this will match 2 keywords
-  ["7: Two keywords matched",
-   keyKey + " twoKey", [8,9],
-   function() {
-     // Add the keyword search as well as search results
-     addPageBook(7, 0, 1, [], keyKey);
-     gPages[8] = [8,1];
-     gPages[9] = [9,1];
-   }]
 ];
--- a/toolkit/components/places/tests/bookmarks/test_keywords.js
+++ b/toolkit/components/places/tests/bookmarks/test_keywords.js
@@ -1,169 +1,307 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
+const URI1 = NetUtil.newURI("http://test1.mozilla.org/");
+const URI2 = NetUtil.newURI("http://test2.mozilla.org/");
+const URI3 = NetUtil.newURI("http://test3.mozilla.org/");
 
-function check_bookmark_keyword(aItemId, aKeyword)
-{
-  let keyword = aKeyword ? aKeyword.toLowerCase() : null;
-  do_check_eq(PlacesUtils.bookmarks.getKeywordForBookmark(aItemId),
-              keyword);
-}
-
-function check_uri_keyword(aURI, aKeyword)
-{
-  let keyword = aKeyword ? aKeyword.toLowerCase() : null;
+function check_keyword(aURI, aKeyword) {
+  if (aKeyword)
+    aKeyword = aKeyword.toLowerCase();
 
   for (let bm of PlacesUtils.getBookmarksForURI(aURI)) {
-    let kid = PlacesUtils.bookmarks.getKeywordForBookmark(bm);
-    if (kid && !keyword) {
-      Assert.ok(false, `${aURI.spec} should not have a keyword`);
-    } else if (keyword && kid == keyword) {
-      Assert.equal(kid, keyword, "Found the keyword");
-      break;
+    let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(bm);
+    if (keyword && !aKeyword) {
+      throw(`${aURI.spec} should not have a keyword`);
+    } else if (aKeyword && keyword == aKeyword) {
+      Assert.equal(keyword, aKeyword);
     }
   }
 
   if (aKeyword) {
-    // This API can't tell which uri the user wants, so it returns a random one.
-    let re = /http:\/\/test[0-9]\.mozilla\.org/;
-    let url = PlacesUtils.bookmarks.getURIForKeyword(aKeyword).spec;
-    do_check_true(re.test(url));
+    let uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword);
+    Assert.equal(uri.spec, aURI.spec);
     // Check case insensitivity.
-    url = PlacesUtils.bookmarks.getURIForKeyword(aKeyword.toUpperCase()).spec
-    do_check_true(re.test(url));
+    uri = PlacesUtils.bookmarks.getURIForKeyword(aKeyword.toUpperCase());
+    Assert.equal(uri.spec, aURI.spec);
   }
 }
 
-function check_orphans()
-{
-  let stmt = DBConn().createStatement(
-    `SELECT id FROM moz_keywords k WHERE NOT EXISTS (
-        SELECT id FROM moz_bookmarks WHERE keyword_id = k.id
-     )`
-  );
-  try {
-    do_check_false(stmt.executeStep());
-  } finally {
-    stmt.finalize();
-  }
+function check_orphans() {
+  let db = yield PlacesUtils.promiseDBConnection();
+  let rows = yield db.executeCached(
+    `SELECT id FROM moz_keywords k
+     WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id)
+    `);
+  Assert.equal(rows.length, 0);
+}
+
+function expectNotifications() {
+  let notifications = [];
+  let observer = new Proxy(NavBookmarkObserver, {
+    get(target, name) {
+      if (name == "check") {
+        PlacesUtils.bookmarks.removeObserver(observer);
+        return expectedNotifications =>
+          Assert.deepEqual(notifications, expectedNotifications);
+      }
 
-  print("Check there are no orphan database entries");
-  stmt = DBConn().createStatement(
-    `SELECT b.id FROM moz_bookmarks b
-     LEFT JOIN moz_keywords k ON b.keyword_id = k.id
-     WHERE keyword_id NOTNULL AND k.id ISNULL`
-  );
-  try {
-    do_check_false(stmt.executeStep());
-  } finally {
-    stmt.finalize();
-  }
+      if (name.startsWith("onItemChanged")) {
+        return (id, prop, isAnno, val, lastMod, itemType, parentId, guid, parentGuid) => {
+          if (prop != "keyword")
+            return;
+          let args = Array.from(arguments, arg => {
+            if (arg && arg instanceof Ci.nsIURI)
+              return new URL(arg.spec);
+            if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
+              return new Date(parseInt(arg/1000));
+            return arg;
+          });
+          notifications.push({ name: name, arguments: args });
+        }
+      }
+
+      return target[name];
+    }
+  });
+  PlacesUtils.bookmarks.addObserver(observer, false);
+  return observer;
 }
 
-const URIS = [
-  uri("http://test1.mozilla.org/"),
-  uri("http://test2.mozilla.org/"),
-];
+add_task(function test_invalid_input() {
+  Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(null),
+                /NS_ERROR_ILLEGAL_VALUE/);
+  Assert.throws(() => PlacesUtils.bookmarks.getURIForKeyword(""),
+                /NS_ERROR_ILLEGAL_VALUE/);
+  Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(null),
+                /NS_ERROR_ILLEGAL_VALUE/);
+  Assert.throws(() => PlacesUtils.bookmarks.getKeywordForBookmark(0),
+                /NS_ERROR_ILLEGAL_VALUE/);
+  Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(null, "k"),
+                /NS_ERROR_ILLEGAL_VALUE/);
+  Assert.throws(() => PlacesUtils.bookmarks.setKeywordForBookmark(0, "k"),
+                /NS_ERROR_ILLEGAL_VALUE/);
+});
+
+add_task(function test_addBookmarkAndKeyword() {
+  check_keyword(URI1, null);
+  let fc = yield foreign_count(URI1);
+  let observer = expectNotifications();
+
+  let itemId =
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                         URI1,
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                         "test");
 
-add_test(function test_addBookmarkWithKeyword()
-{
-  check_uri_keyword(URIS[0], null);
+  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+  let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI1 });
+  observer.check([ { name: "onItemChanged",
+                     arguments: [ itemId, "keyword", false, "keyword",
+                                  bookmark.lastModified, bookmark.type,
+                                  (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+                                  bookmark.guid, bookmark.parentGuid ] }
+                 ]);
+  yield PlacesTestUtils.promiseAsyncUpdates();
+
+  check_keyword(URI1, "keyword");
+  Assert.equal((yield foreign_count(URI1)), fc + 2); // + 1 bookmark + 1 keyword
+
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  yield check_orphans();
+});
+
+add_task(function test_addBookmarkToURIHavingKeyword() {
+  // The uri has already a keyword.
+  check_keyword(URI1, "keyword");
+  let fc = yield foreign_count(URI1);
+
+  let itemId =
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                         URI1,
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                         "test");
+  check_keyword(URI1, "keyword");
+  Assert.equal((yield foreign_count(URI1)), fc + 1); // + 1 bookmark
+
+  PlacesUtils.bookmarks.removeItem(itemId);
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  check_orphans();
+});
+
+add_task(function test_sameKeywordDifferentURI() {
+  let fc1 = yield foreign_count(URI1);
+  let fc2 = yield foreign_count(URI2);
+  let observer = expectNotifications();
 
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         URIS[0],
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "test");
-  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
-  check_bookmark_keyword(itemId, "keyword");
-  check_uri_keyword(URIS[0], "keyword");
-
-  PlacesTestUtils.promiseAsyncUpdates().then(() => {
-    check_orphans();
-    run_next_test();
-  });
-});
-
-add_test(function test_addBookmarkToURIHavingKeyword()
-{
-  let itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         URIS[0],
-                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
-                                         "test");
-  // The uri has a keyword, but this specific bookmark has not.
-  check_bookmark_keyword(itemId, null);
-  check_uri_keyword(URIS[0], "keyword");
-
-  PlacesTestUtils.promiseAsyncUpdates().then(() => {
-    check_orphans();
-    run_next_test();
-  });
-});
-
-add_test(function test_addSameKeywordToOtherURI()
-{
-  let itemId =
-    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         URIS[1],
+                                         URI2,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test2");
-  check_bookmark_keyword(itemId, null);
-  check_uri_keyword(URIS[1], null);
+  check_keyword(URI1, "keyword");
+  check_keyword(URI2, null);
 
   PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "kEyWoRd");
-  check_bookmark_keyword(itemId, "kEyWoRd");
-  check_uri_keyword(URIS[1], "kEyWoRd");
 
-  // Check case insensitivity.
-  check_uri_keyword(URIS[0], "kEyWoRd");
-  check_bookmark_keyword(itemId, "keyword");
-  check_uri_keyword(URIS[1], "keyword");
-  check_uri_keyword(URIS[0], "keyword");
+  let bookmark1 = yield PlacesUtils.bookmarks.fetch({ url: URI1 });
+  let bookmark2 = yield PlacesUtils.bookmarks.fetch({ url: URI2 });
+  observer.check([ { name: "onItemChanged",
+                     arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+                                  "keyword", false, "",
+                                  bookmark1.lastModified, bookmark1.type,
+                                  (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+                                  bookmark1.guid, bookmark1.parentGuid ] },
+                    { name: "onItemChanged",
+                     arguments: [ itemId, "keyword", false, "keyword",
+                                  bookmark2.lastModified, bookmark2.type,
+                                  (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+                                  bookmark2.guid, bookmark2.parentGuid ] }
+                 ]);
+  yield PlacesTestUtils.promiseAsyncUpdates();
 
-  PlacesTestUtils.promiseAsyncUpdates().then(() => {
-    check_orphans();
-    run_next_test();
-  });
+  // The keyword should have been "moved" to the new URI.
+  check_keyword(URI1, null);
+  Assert.equal((yield foreign_count(URI1)), fc1 - 1); // - 1 keyword
+  check_keyword(URI2, "keyword");
+  Assert.equal((yield foreign_count(URI2)), fc2 + 2); // + 1 bookmark + 1 keyword
+
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  check_orphans();
 });
 
-add_test(function test_removeBookmarkWithKeyword()
-{
+add_task(function test_sameURIDifferentKeyword() {
+  let fc = yield foreign_count(URI2);
+  let observer = expectNotifications();
+
   let itemId =
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
-                                         URIS[1],
+                                         URI2,
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                         "test2");
+  check_keyword(URI2, "keyword");
+
+  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword2");
+
+  let bookmarks = [];
+  yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
+  observer.check([ { name: "onItemChanged",
+                     arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)),
+                                  "keyword", false, "keyword2",
+                                  bookmarks[0].lastModified, bookmarks[0].type,
+                                  (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
+                                  bookmarks[0].guid, bookmarks[0].parentGuid ] },
+                    { name: "onItemChanged",
+                     arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)),
+                                  "keyword", false, "keyword2",
+                                  bookmarks[1].lastModified, bookmarks[1].type,
+                                  (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
+                                  bookmarks[1].guid, bookmarks[1].parentGuid ] }
+                 ]);
+  yield PlacesTestUtils.promiseAsyncUpdates();
+
+  check_keyword(URI2, "keyword2");
+  Assert.equal((yield foreign_count(URI2)), fc + 2); // + 1 bookmark + 1 keyword
+
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  check_orphans();
+});
+
+add_task(function test_removeBookmarkWithKeyword() {
+  let fc = yield foreign_count(URI2);
+  let itemId =
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                         URI2,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX,
                                          "test");
-  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
-  check_bookmark_keyword(itemId, "keyword");
-  check_uri_keyword(URIS[1], "keyword");
 
-  // The keyword should not be removed from other bookmarks.
-  PlacesUtils.bookmarks.removeItem(itemId);
+   // The keyword should not be removed, since there are other bookmarks yet.
+   PlacesUtils.bookmarks.removeItem(itemId);
 
-  check_uri_keyword(URIS[1], "keyword");
-  check_uri_keyword(URIS[0], "keyword");
+  check_keyword(URI2, "keyword2");
+  Assert.equal((yield foreign_count(URI2)), fc); // + 1 bookmark - 1 bookmark
 
-  PlacesTestUtils.promiseAsyncUpdates().then(() => {
-    check_orphans();
-    run_next_test();
-  });
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  check_orphans();
 });
 
-add_test(function test_removeFolderWithKeywordedBookmarks()
-{
-  // Keyword should be removed as well.
-  PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+add_task(function test_unsetKeyword() {
+  let fc = yield foreign_count(URI2);
+  let observer = expectNotifications();
+
+  let itemId =
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                         URI2,
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                         "test");
+
+  // The keyword should be removed from any bookmark.
+  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, null);
 
-  check_uri_keyword(URIS[1], null);
-  check_uri_keyword(URIS[0], null);
+  let bookmarks = [];
+  yield PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => bookmarks.push(bookmark));
+  do_print(bookmarks.length);
+  observer.check([ { name: "onItemChanged",
+                     arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[0].guid)),
+                                  "keyword", false, "",
+                                  bookmarks[0].lastModified, bookmarks[0].type,
+                                  (yield PlacesUtils.promiseItemId(bookmarks[0].parentGuid)),
+                                  bookmarks[0].guid, bookmarks[0].parentGuid ] },
+                    { name: "onItemChanged",
+                     arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[1].guid)),
+                                  "keyword", false, "",
+                                  bookmarks[1].lastModified, bookmarks[1].type,
+                                  (yield PlacesUtils.promiseItemId(bookmarks[1].parentGuid)),
+                                  bookmarks[1].guid, bookmarks[1].parentGuid ] },
+                    { name: "onItemChanged",
+                     arguments: [ (yield PlacesUtils.promiseItemId(bookmarks[2].guid)),
+                                  "keyword", false, "",
+                                  bookmarks[2].lastModified, bookmarks[2].type,
+                                  (yield PlacesUtils.promiseItemId(bookmarks[2].parentGuid)),
+                                  bookmarks[2].guid, bookmarks[2].parentGuid ] }
+                 ]);
 
-  PlacesTestUtils.promiseAsyncUpdates().then(() => {
-    check_orphans();
-    run_next_test();
-  });
+  check_keyword(URI1, null);
+  check_keyword(URI2, null);
+  Assert.equal((yield foreign_count(URI2)), fc - 1); // + 1 bookmark - 2 keyword
+
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  check_orphans();
 });
 
-function run_test()
-{
+add_task(function test_addRemoveBookmark() {
+  let fc = yield foreign_count(URI3);
+  let observer = expectNotifications();
+
+  let itemId =
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                         URI3,
+                                         PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                         "test3");
+
+  PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+  let bookmark = yield PlacesUtils.bookmarks.fetch({ url: URI3 });
+  let parentId = yield PlacesUtils.promiseItemId(bookmark.parentGuid);
+  PlacesUtils.bookmarks.removeItem(itemId);
+
+  observer.check([ { name: "onItemChanged",
+                     arguments: [ itemId,
+                                  "keyword", false, "keyword",
+                                  bookmark.lastModified, bookmark.type,
+                                  parentId,
+                                  bookmark.guid, bookmark.parentGuid ] },
+                    { name: "onItemChanged",
+                     arguments: [ itemId,
+                                  "keyword", false, "",
+                                  bookmark.lastModified, bookmark.type,
+                                  parentId,
+                                  bookmark.guid, bookmark.parentGuid ] }
+                 ]);
+
+  check_keyword(URI3, null);
+  Assert.equal((yield foreign_count(URI3)), fc);
+
+  yield PlacesTestUtils.promiseAsyncUpdates();
+  check_orphans();
+});
+
+function run_test() {
   run_next_test();
 }
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const CURRENT_SCHEMA_VERSION = 26;
+const CURRENT_SCHEMA_VERSION = 27;
 const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
 
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
@@ -846,8 +846,22 @@ function checkBookmarkObject(info) {
   do_check_valid_places_guid(info.guid);
   do_check_valid_places_guid(info.parentGuid);
   Assert.ok(typeof info.index == "number", "index should be a number");
   Assert.ok(info.dateAdded.constructor.name == "Date", "dateAdded should be a Date");
   Assert.ok(info.lastModified.constructor.name == "Date", "lastModified should be a Date");
   Assert.ok(info.lastModified >= info.dateAdded, "lastModified should never be smaller than dateAdded");
   Assert.ok(typeof info.type == "number", "type should be a number");
 }
+
+/**
+ * Reads foreign_count value for a given url.
+ */
+function* foreign_count(url) {
+  if (url instanceof Ci.nsIURI)
+    url = url.spec;
+  let db = yield PlacesUtils.promiseDBConnection();
+  let rows = yield db.executeCached(
+    `SELECT foreign_count FROM moz_places
+     WHERE url = :url
+    `, { url });
+  return rows.length == 0 ? 0 : rows[0].getResultByName("foreign_count");
+}
index b43dc2389e4a17471bc01ae421f9521fbc2a0afa..b4b238179736aa64b22329e9c8ef3b2a69d3effc
GIT binary patch
literal 1179648
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
z1^@s6000000000000000000000000000000004kLItQz3>q6J8YK&J`hr^W%s&^C$
zL-`Aunnp%O77b*_(wR(hQ8w4#lp1W@vbm`*olEs)_cRTr`*##FqosVlkQ*LsS}|Ub
zST|mfSWzlSY#JU+Y)JN}`g7Su$xJ3amK|7B>bT;WP({4|<|lW2@XF?0yEd%5Y#`sa
zthuMAE|d*5#^cqYXPe0}=WygGz2k$#?rUALI(^x)vG&b-S_XE;>q5bE4>FX?UY+VG
z<fle!ZC})!Y<aH1etXNU8<(Z8-nwm1-_8w}HC|X<7upyKRTl@V4lRh6|1kXprN94v
z(UP<B*<9hGmh;XUNbcE`$`5A>`HNbbbGw&!t{i;T*y_vLTMr&rH8*r{^vLFY*-URL
zchR!->sPPs*tB|c_u9^NTh5w@Zdtct)tb)b-BYEL1zqdcuUXcy>8xGDsoZE+X$CD5
zGgv%x24|)A^kjy6QyYsF)2U))>&DEIj<!s9V^4ZbYh_)i<H!k>yIDH3mvmcl-I~i6
z_h+vdXxvay7uphPtS)Y_NU_ngZ?NR{>~P_z#f^ooZ`(QAvNXMFXxG5z;VrScQ1|og
zu_rrN$fdUr7g9%c8x37QHrmrMv~pni+JWUAuZh-$2A*%Xll!7)IA6#Pq{oi#JrcUU
zW5ZBa_u{c#eLLFHTO)O$o^tQ;=imE|RA%U?jziZcyEiQ9+&HwpyQAasE5dc5bh+Qq
z?=b6BaZ!?)S-KwXD75c5zx(o`#>>)m#rH!rxF;0+f5E>N6951J000000000000000
z00000000000000000000_(OI=v?>%1SB9%2wV{f5|IJVC_+Z8Hk*ZL<6ss8z#qO&(
zE?gC=4p+vj<K@tAZ@IOu)cU{+JbM5D00000000000000000000000000000000000
z0C<7blnw`8;7I@g000000000000000000000000000000000000Kf~ZuJ{uU9t{=$
z0RR910000000000000000000000000000000002MpMn#^m7y~u)uDmxSUQtQF3RTm
zL)X_73&O!qL&bjp00000000000000000000000000000000000008i(V{Y`!_&|0n
zoyjB@Wpn*?#n8OHp9lqC4c-}C6*TYv+5S)L-@E^n`@{S0-S?OKUcK+7dw)<Y1^@s6
z0000000000000000000000000000000001fkDWVjPAD7-hl{6`l?$SgNT?#-fAgbP
zgeKGO-DBG}tjwiXWR@n|=FSPl!{PXu<;vBOO{t#LU?I_!?9Y!^-*WO~v!Uh9ySuI)
z8d|b7*LYD~xmk5<xmndjv&?8>SvI?KAeq~l>K$*kv3ata?iHI?CbO-%T?1X4m&_S&
zzM$N^GP*XKFC@06^XbBo?KU<qUz1(7F55PA_3lk8TWibh!hcq7S5X!1*q$9OB(_Y<
zwe7+2^!(3`T$$<ITo@bNw!ACVUNhcke6UzmWM!(5ST(pio7p`%TFXx+J7{lD_HWOu
zSzB0@Y`HK#zBWzc3t4sPaK4ZoNROowE7G}CUv|%Uqeaz|oeb=19$J+d9=>$*&g3hq
z$1ky^+&EHoSt>J>NDlTUx`qqcTsoPVnDbdn$J3W5T07UDpW3!`<Erk_)(fh}@8P-Q
zts})1D*s*jnIliOZF?|zb?egM!tTzkJKK7fRu)H&hvVaIqs6N&qzC&Gn+wU@)bgIT
zV>~@$Ln@ygSzE|oxnpU5+bb$cqsGG%bB=Bq%x6n&rta$aoPW4}V!7ttJ3QEz&ZK&~
zOBTh(Tc0#B=czq3S-+STim~nq7eveTCzdxdHAbQ<o6T%b=4OtyvAHlFF1Kxul-pJ}
z9NBhlYH)aF!z)4q#n|Ma4dGJ5>gu}jhDXjXm(3O?s}`$-DuR1L!7qYGgNK3#il+bo
z000000000000000000000000000000000000Pu&Zwj%nH@bF+?I+N<HsE$QX3Kx?7
z`B-hV7%pV9neEA3v^r7@52OZ%Bk^$bl=x64*^|oG6dP9rUkU}k3LXm{4jwF?0ssI2
z0000000000000000000000000000000002MAGF%&OTxp0ed$c9x1w}j$YwL!leuVh
z^rUbh*`JTaqo>4&GRdA)z7!fr4Gu@@N(~RZz!L`m0000000000000000000000000
z00000000000Du=*P3dsp1)c-|00000000000000000000000000000000000006we
z>Pm+LFYqJ)00000000000000000000000000000000000007_xR#*I~*!AhqzHMV4
z*;74yRq+%6000000000000000000000000000000007{R$D0xr@zBAOuHSQd<$@iB
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
z5ARH^zoIL(qo=g9D)xUg6nr)af>-W;s(1<j000000000000000000000000000000
z000000KgxHRk4F7g?AJRL-`Aung+6C*<Amkp5DPl#nW^qlUy{IDl`qGdeh0K^gw@8
zUpklS%kF9FOYTnhWCs@&4;L?PZfR|7URrTU>|nIqQ?a9wk&&q$iaj<hZf<UC8c6o0
z8i$7(CmJ`kHnyDC(%#<E*1ov8?fmm&zjFtRXLPVM`a5^fGNXg`$i>AD;=fA=%`-Y!
zQdeBiNN{&3_*U@%000000000000000000000000000000000000001g!0IE>P}_z~
zvL}^q+B}fV6_#bQJB#Vg`Qd12)%J<ZhFo?il`Eu+m3mY8o?LpUkj@U)6x&CFe+mUZ
zDjom;000000000000000000000000000000000000N@YV{OGFq_H1_NKr**;LoPd%
z$`#V7d{b{K-;+xZ71G(klcR0n4Vh$5F}k@}tFUaMcBHP<{=f@7e*gdg0000000000
z0000000000000000000000000c!AXwe_}yRD0o}2BWNq000000000000000000000
z0000000000000000002+C$1q@8w!U*^Q*(*%F4NsO{t#LV4*A7pN~aD74iO?AKmte
ziS$z~(Q?(6a@D$ts+rMc+3e1NWNv4wce2UWGbhpoFOQU)oLO!%C$cu1FKkWc(}lxz
zH@{;d{kyBfb3);G=-_zeXh&~vV*5nb`9vX`DC|h*6MflCZz@;H=2NL+C^eAiP3MO)
z$<b7AB0E@$B$iFJ>dIy_+mpG;vHmPRk=E7~ul&TlO`+g3!JC71!Rh;dy8pBL-?o3}
z{!90tyzl$_{$XEk-|~I)_I_pWyZ7!YRs#S4000000000000000000000000000000
z000004!h*USnCxh4rIr=r~V>&cP^VP<ny~S=|ZYIIb6u52YXX{x@W1=-O`q5S#(0|
z><dqsQ983~v73Q%7sc9(6N{UVkDYz-v(+C;=2C-&$ri1NmJ5!HoxS<FM#v=dg@J5u
zx-XsToi<wQyx7^RPI{KM{ln?r?%~1ouHn>VvzCQ(W2bkP7iC8_e~hIlmvpzZ6sK1g
zYg$`hj-~U3Y;JUS>F>bjj|y}TWs*Iq-eh5XpyGukT3YACnl3%@d9>}zWd|mkwbnk*
zM$^Y?sEN(Lu)GlEY2*)|b@wHAr+c!4<Cou(SbSDIHotB5s->%*theOs>e&4D+3S@j
z(32e=ER-s~yec;TqGODmOZB7%dqyWaZEdQI&0jft-O?>CcRY1nr3MQtV)Hv@Z!nz8
z%(%711#gVS=3hE{?W1-=>E1?T=WZ<D+tMD(cP9r2v-u;k<@02rkV|hLo|xBprF*+L
z5?ipke48c~c=nAnSy+CUE(uRQj-{@<r+S^e=@E6hTNWo;YKjkaZSc8J@MQ3-;Ag>O
z!6U)<gNK3#gMSXbR15<E000000000000000000000000000000000000KgxRifA}o
zQCm93DoV#_taOY-W6^jx5{rhyHKjwmbf_*Js!E5-XfzbBDYmW+E)NC24SpH?H279<
zZ}8FJUBPuhA?PlK0RR910000000000000000000000000000000002scl4rYW4J$;
zU6jmZ(qq|yMI$34i)v>J*31@+&laqhEf||E7@aK`IWgK8KdN~&6s|5Es!E5-n&QlB
zgD-@FCxc%FKMNiU9tpl5JQRFAcp&)S#V`N>000000000000000000000000000000
z000000Q`Z8MdRVx(xIkwh{vMU;p)<%s&uF<9V$wPSm_Wg9U{?ab-bq7vNrg1DELkA
zi{K~0qrne?ZwFrw{yF$!aCb2b000000000000000000000000000000000000002L
z|0<)AaOnDq@gz2$M8}gzMKlsGmPNzi-kS0;9*b6ot4oKf(xI{xe5SUh*iCKlkD=hn
z;8(%Vg2#eKg6{_p1z!)o6x>k^0{{R30000000000000000000000000000000002M
z@2NS_%5bP6-hcCxJ3d%@I1{_CW-9aBTW*cVqM>kg=}=WVRF)1Er9-TAh?WkKXfzbB
zDfSo*9tZ_L3%*}W000000000000000000000000000000000000002skH*|sxH4QD
zs)+aB{N#=gM$6fn@oenANL?`-kC(H*z2(-LV*P0F(@^ka@T+11000000000000000
z000000000000000000000001g8Y-gUa3mUw#*4>jb-bn+iU!{Z1^*HJsF(l%00000
z0000000000000000000000000000000KgxaInl~+s3P8f^OHM17(JYc-505iR>sSj
z-`;X-O|j;@;2ojhH^I+?e-C~TJQRE_xIg$pa9419@bTaug7*hE6^j7?0000000000
z00000000000000000000000000N{mD84Jh5)pN^9Rb4r$oKsFJYRgHirkq6M<s?#F
z5sMeARK`N#aCBZdiBydz72`>4Jc*7ck&0pxkJJ>0niu>{DEMvgi{SC#k>I<*H-fJO
zUkvUFZVx^l{6p~m;KpJx000000000000000000000000000000000000002I5UOI~
zaJVX3JDx^rDr4bzxGG*us>4;)<y^F~oQuvW*NH@{#?#2Wa%p64xinH&8H<O*(TZ{s
ziB-hn#g@fmu_zX+u8!9f2b&k16AFGF{2=&R@cH1j;MU;A;H|--ARqJwTY?oqd$AY*
z00000000000000000000000000000000000007G7#=@21>QF_z|K>-p2+b{LtH!fi
zPOd9wE61}No9C3X7310SKU-VQ#>TU457v~k(eZ4{PvYflWIX%wM0H&(UKt+Gow1>^
zE*6^1T@ji$BNv&L+t?hPmTP;kYFcjYy%mRZKV1Kl*qmZ6KAEd2UR&+Hn?k|Yf}4Yp
zU|Dd&{(sqj)Bf%Iez@<RVi*7b00000000000000000000000000000000000;P=K^
z(T(AuOtL3+L1A<#b<vWu^4VPBqL%Z{8c6Qhl*$ih3i*p#nyXKXc1%<&moJ@JTzO6G
z;OInoUpCX5%3ZW<{rc5wJ2tJ}+`YDQ-IlW^qFdIjShc2edG}Q5WI@;Z^=p=OY&vV#
za4I+2Rhmi5#7q{?9O|spo}SEbZ)#()VmeifR9zGe!aE9uq5K6+O(P>Ciw3e|=}acM
zD4XkV8p>s_PW2S>O?~NHsxP~zskMDkbF!tB%@=aRgH7?aX!e+`CmRi>`*##Fqla77
zTo}FPn5|NSjaxP!yKQY#wC8!Y-I2-+HAT*g4m{8LJ;V7zb|5{LYKpc*)A8roC^cBf
zB{NNp(eCFMrYBphnchBJNHxXIiEeq0+R5$N;X+eIO>vLa1}j3r<H1A0mxJ4bTY|q0
zt_$`A{lTk>VE_OC00000000000000000000000000000000000_#GS<T@W4{?dcd=
zIk0@~!19jRyy$}Xa7Uqi$NAlt4>ewvE>+0wUf#KK@Ks~0FKchDE(OwU$#rWkU)-O)
zVxY0I6i9Y&Skk$1Xnl7_$K{by;L7G*yEd%5Y#`sathu%n*t#*Zq@yj<-Pn^}Q&kFV
zT$a9i>$W|8J2za`7%v4nHVk!jFCN>~x1%i`Ed|<kj<zgKuNvAluz9$m6lh(tI(^x)
zvG&b-S_W!rinE^=L_)#i!9&59gWH2!g1-%}3-$#4!Pa0!&=$NbII&m^0000000000
z0000000000000000000000000006)rz2l+_!XsB^IyV=_2DdHmO4XJE!-IY4OscoL
zq@olkWV4y=$y_-QEeDeQ`R)m^dC>*&TsB*n43sJiEpOi4b@kBDlC8PMxuw9suI8at
znc?9}H}6cw%YoG3@MMcfDUix%N7fedSMFGv-&R=)B(H8=I$YS@xpik-Z&fMKy?boi
zhLySWip<jFymDa0=9S58Yi`#-*XFuXVELNtvUS<Ep{sXqS~;f_Xm3yUZ_lh*TUeEB
zsVN0oJJ+9|+O~A#s_xO&>YCCL9C(3m0RR91000000000000000000000000000000
z00000yufOTKe6C@q2M>c&w?KpPXGV_00000000000000000000000000000000000
e;Ez&OEF2Ecjl{}nT_jpg=R_hEv2Z*d{r>@kMqVxe
new file mode 100644
index 0000000000000000000000000000000000000000..57dfb75627d10e9a246b91573ffcae357b574f0c
GIT binary patch
literal 1212416
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
zdbLq!4rUea-+V~lP1jN@DwOrkvW6bomRg!F!K|Y03wB3z*BDf$U|wV>D{|2b5#7`+
z4NbwU!lSvv%)wC?oZ?jj000000000000000000000000000000000000000000000
z0000000000003}`MFxWfr+5+o0000000000000000000000000000000000000000
z000000000000000005j~X5~jG->;HCmv_t0D;EF&0000000000000000000000000
z0000000000000000000000000000000Q|Z2)#+8qYNN_(w~HQMf?I5Ix?I)-(d)2x
z)9O_Nlp=>HI$T16Q?hs0=vA@Da%`g8-czktoux6V+BH-yi&Q$+$<L_dPvy7dt;z)e
z00000000000000000000000000000000000000000000000000000000094KqP1t4
z++vf{<+3J-UWZvbK<yAkhf7FsO4?{`Y-`FUx;3Y1lhszcUG(@8+BMOcm2SGueL^Mg
zly8w|$Vv0Rng7K6x%1DRub%h9yldwDW!@QcKT?VT0000000000000000000000000
z0000000000000000000000000000000Q`v!?;532tJG@cQm-GZ)o4@^(bEcxb|$sP
zWsRAo$x{n5d}a1PdUTY^q*j~G3RO00$_1O?kt`Kfhc8@x<2~Vc+l*;`u{`JeX-QUp
zL(*7tsF^V})XdP*%++Yg7R5TZ)mtan!_6MFgyVIGGF(jswHXcmDo0g)MpU@@;81hD
zwpjE@mMKo3Q)+E@{lnpSb(!B=9*{<jZz^%t1yZ|&+NsYBwTm!lv#LeEWT_0#b>hZD
z;dp0*vvFqQ`HfR^tu`TJWMsHecrcwolP5@)0?%yGHM?cBiB}E_$E#PArPf&+0?Fl>
zd4aq!rtsb*hBwkM-tUt{x3fvGOmKRI8nGeVsJ2fyp0g^yp>(>-S2^2LQkrs(F}%d&
zP-BfDUvSl1tRB0i!Y_$lr`6Rm=lBKTxZ<kPI;X3`U*1@l=}I4M2;alu;no^uheH1_
z{qZA5pVzwXNmI)00q68Waa4glQ?HC{Qk%kUwaRKGr^jKLELpv++gtEqemMT-Kwy01
z$cD06qpC9sGtY?#j%rf3%voFM@rl7UZFe;^=ZQy_{t#|GF4OO+ak>P1RZxO1+`4zm
zoLirv*80I%Qu3-=jMj$gw`{MbZ466=D7vbx-s8qn9tbI%9cnvL6KZRWJF#uC;PD^d
zaA%TR$!i@nP91D$G@8Q=Pn@4u6s6XxN)=UvyhSDdQ~q4uE5D&!0ssI20000000000
z00000000000000000000000000000000000000000002+pQ=lQwx8PXsd2gldxTM^
z?X8xq4xg@zR>_t`(N%5rYK<Bt+bwwf8k1Vv*HrJa+5}&u(l|ois*(@OU&{OC-O42Z
z00000000000000000000000000000000000000000000000000000000095dx@h~U
z{hk`9ORz@-uO(4*Ra?DUqqetNvO0V^leVv^-et83zF?MH@c1?6V8aEcc;Wy600000
z000000000000000000000000000000000000000000000000000Gwix!C=8Do&*2@
z00000000000000000000000000000000000000000000000000000000H>Ha7%Vu&
zlK=n!0000000000000000000000000000000000000000000000000000000;1n|}
zKgOA>Rr9vaS<z?@B>5hwH+d423;+NC00000000000000000000000000000000000
z0000000000{0F-@!e~?lmQ@eX53ZG@df({8M62B{dVC2syC=adHaT4`Yl26R5`B_Y
za@rD|ZbxE`b+*$cdJ>f2lCqLKbL8ofN>}EB{G4(n?iTzp>XmT7x4&AINaxEgs4yz#
z8tqG!w5dLSnop^}EBK?%9(HqslG#_vohnThYovhHE2sj2K!Vd#=d)Ssg#^*-xcRGd
zPFJZk_Y~x0Rb*a%`89>L*E)JFS$Xj0gD(sE;+a3EUy^)7cEqGh%oh&2@w}ZEO}`+&
z`vvQIJ$m5vgwy@IDlhE&`UlzdwwQstYnB%~%X?K{)VKGx6=k`f3q@lVKbIC`OZnCQ
z@ycJvB=ktkJiGrZ-;CYf*f=Wh663-98m@Ft>Hf<5wR!u>q6%LKT(Z~lZMJRiDDwlC
zS04WL(9Xn_J9njBllXC;iMNi~J#Oym{<XhaHk5m;A1?cKz>8bOA2z0*e*g4!+38O|
z_}W2j=FoXJlplThvLlU`TAP<Yb^i66l79Z$e|b~tqJ}Hq{ac!2^J}`Y&vzb~GrDWe
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
ziuHn5aw?VVg3so4)=N&&6REV<$eUI2CrSVS000000000000000000000000000000
z00000000000000000000000000Kk8;SZ#r+S`_QtR&QOISF9Jjl2h;{+6AA@>#UcY
zqNk5GU0vp~+LYYMN-Zh7rMAW#Y`@?X&mRB)000000000000000000000000000000
z0000000000000000000000000fK$w@{OIIJm3*ySE2k?L00000000000000000000
z00000000000000000000000000000000000004mhadEmXDz#b_YgDWC`e;qLU=uu2
zh1KEHX;l%?(+Z1TY-o*>wV|rXp{nMVs;<UtQLJ-Yy>)`UwaL0et?}8KP?NJlO`<f#
zqEDLQ^f{$tb?>Ncjc2N(RBDr|Ib2zrWw%?ZTe|jHB+(+(I(?QJ(PbCB!K6<Rlq|t*
zu{(YBE^DJ;w}_r#jwQRTRfQ<Js;%CZu?n6uw#H^<<vKZ8C2y6N%QKV<0000000000
z0000000000000000000000000000000000000000000000002~h<KfD=$II{*i=<5
zigj+Qx6XH5qRJ@=Zl}jCG^AUU{}Q#jq2qq5dcD;vc%+sFsg~qX8r{&zzt_LZ>XY1}
z-C5%l?8nARQR{{l^!$yc4!_f0<@Y#e`GuBN$&t#EqUCie`DgjC{Ehsjd_ewC-Yf5s
zcgWl1E%HYBDWwnq00000000000000000000000000000000000000000000000000
z000000DwQE2(4Oej1Gndb1>9*35L3eV5rpvLycCaHK{c^tx6pg3?hSpDHs@ofgu>^
zwOW-aQt2*Q?xK=^lE0U~miNe;<%i{)<t6eg`2smlPLca7g#Z8m000000000000000
z000000000000000000000000000000000000001h|0O-Nacaq#Ah;Z&=tu|z0tx1x
z+Tm)qXltJr)k_<%c6h}EtIOqV65XN9E*)h?c9d!AC^Mp?OkGEr+Kw_cJ+<+s6EkD9
zsmE3Hc|^P2nh>qis?^3{U<d~KNM+&C@>44LXZf)Fjr^s2K>kqPEANrtkhjY($(!X3
zN+AFM00000000000000000000000000000000000000000000000000000000DoRO
ztw|jn49vkGDj0ML29d$Qq|+MJ#$aFw2KrzS5e#&}KpPA+TCLF(sdN-A->;H?l>aFo
zl0TO}lHZf}$UEe1@)mi6yk34(DFgrj00000000000000000000000000000000000
z00000000000000000000;Lk&^)u>g=BEpd_9BIRmCPJ$*DP>x<+8!MW&7m+V6h?+Z
zlTK??8-sx%80dqUzjldKI*yhvQ^`NehvjeNFXaRBhw@%|kGxg>yZoqpm%KtL1ONa4
z000000000000000000000000000000000000000000000000000002s|36V$y;>C!
zJ*`Aow;{SUQM`ZiA#+>eon;MCt%;)U3wC#DOGGYuA+j}5cr<sINvBn*jlsYW4D`Vu
zA{gj`fi@Uuv|5!ZQW;n;->s6rkw28*kT=VZD+vGq0000000000000000000000000
z0000000000000000000000000000000REh!b!xpjS``sJtwdP2K_5z*gURCkn-4{V
zl2M`LJIfk$p=6g}vZ(ul-P%wxGL(#5^n%8$Bu%DZvhZl`ut=qIy}VB)|12MtzmdO`
zKT#3@0000000000000000000000000000000000000000000000000000000008{A
ziO{Omx`<$?(dx7&wMM5ks`bG@r_~xwkxCW4yjCTDCx0TpC2x~AC<y=n0000000000
z0000000000000000000000000000000000000000000000RH@<w0gBFB6?bhux^9C
zHBr2O^Pz~g#5>CxbghY^?hAHn+Y*tBUeI*W>P^8!;nCb-kxGYM<r`J<kMg(j7xG8)
zUU`@Ns{E3?Nq$;hCqF3PEw56F0RR9100000000000000000000000000000000000
z0000000000000000000uHS{{QNo|Y{MFw*y(np1&h%TW>7a5AQrck6YM(9jR6}?WS
zR%^S4B8?#&MT8?=IMRkAO@tDeG?B_sUF928@=x+V<%9A8d7u22yhDCj-XuRQuah5?
z@0M38#Q*>R0000000000000000000000000000000000000000000000000000000
zoC*e=TCFx{yM$v+q+X{ssSPG2GO7*6P)e&0rL<9@IvTAZ9BaCUN;T1;QjJ-!GpW_u
zh)|@_Md(aQOC?l_bULHa6sZi>RUW32zm-3dcgZixPswZKmGW|Vq3o0Ga-}>$9;p-q
z0000000000000000000000000000000000000000000000000000000000mYtyAmO
zMpZ=gw8EloGonLDLpZtakU5mphm)(zqC&}tP;%nVhAyF`E}UGwA~KZJhLdwvnL<fT
zI9YL((X2D+)uB|uHwW})ovI~Ov@@yeaVbr^)b$T*kEJGVJY+bQDj1g;(UzKcWa$sO
zC?#cTO+^ORHuq7L{CBxg?lJ$<`A^NiWPZi`LGupGdvu;O_lLQAltKUi0000000000
z000000000000000000000000000000000000000000000e^NuVlhpMtt4$a!HP#Db
zGluv?uQWC}ZHU|2P%ilVF3C4GImy^xo7GY+RGxW!v3`!Oxv{0ZMs(Q)@7V0p(!%1b
z^1{hg#knPwLt1hxOC}T)<>pkim9`dCl$I7{XO$0`<rln-6~UP#x6CBv_@Raf4K|nG
zE=*D?It3-iFjgz8YbB}PH##vf5C|l=#U`iAWla#hj>LMeI8(4mzQh`*SEvyi5>qo1
zlB~(Wq)+ntJ&C4tt=MVn)<zzuqgHY?9%~gjMmwj|R)QzKa&qTwyCiCDf3R(>;Hpp5
zq-ouMu)fXjlSH?(Nl4TtYn`S)*huh5UaKoHUR(7$!`MWnrnA~F35mL4+RERlZLJpl
zQes4nuDSHMnhsHPxP%0!l;{(zUR&+BSz~>(Tuw<C?zVcY4#7LbDU9_?HSw84#MTQ(
zz*pt9kF|M+2+pyY>G>5?iYBLJmsDG=^{D|zi6h&bt7|rQG>lDj|M796&eJt(+j_Cu
z?V`tb+{%<mw{BagG%?&`d`pjs(UHoDik5q-<b(2Fd7J#Se7}5?e2Ls3JLD;HuAD9p
zQVIb800000000000000000000000000000000000000000000000000000000RM}+
zX$Pwtrq*USvhrQowZ)#OU`9ZgIBNW~Tp?vjRhq7=cCcx7Mv2SocTFEvR+18|;3=M6
zP&M8=scQTLuQ8ZWGa)xCZ$dy!%PA|04rWw~6H{#sxfQb~rcaCrW=t<nO>3Ge=8eyp
zEog!nm4$Vsb>pjqn$qfgb1);{nd_*{PRkKz+N(^#j8Rh4NMA~Psg&bPjSOa_mkZU&
zjn10fnZ=V0p^TD=BPW-Q&vH+;`Lw}|WVe`CUnbNPW~AA=1T*6E19jz1x#y>s)lJez
zDhuu^YgF<<d9S=pep<d?zDd4BZjc@F`SJugT^=O&REhxr00000000000000000000
z000000000000000000000000000000000000RQ#6X$PwV;~Pgdl+7Adol%&n4`%qi
z<pF8b_@)wPT_7ry;i+-D1bbCbL@+}VMOU@e8_LjzGOP|?RSR8L?O>Bv6s6XTU<J3}
z@wcWm!3<~P%*OK@r{-F1LPnQh#x%cJo^$@RB&)w6DLR-jX-c_0;GAA4jw-Mlf*ECv
znWf273o?9V_CVKAMrxh4A&^|2nHR{53}%$pIb9Y0^2WkUSGqBnk>5}{-Q}yC?I|fu
zF$FU+TulYF84dm_M^(K!GPnf`PVp@O00000000000000000000000000000000000
z00000000000000000000007_=iwp(}PVpoF000000000000000000000000000000
z00000000000000000000000000020}B9$MV{DDgTQT|5$Ot}C6000000000000000
z00000000000000000000000000000000000000000N}rpL8n%$qcyrvY}RN)ag;_A
Kp;Mbo+W!ZK1wgC-
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v26.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(function* setup() {
+  yield setupPlacesDatabase("places_v26.sqlite");
+  // Setup database contents to be migrated.
+  let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+  let db = yield Sqlite.openConnection({ path });
+  // Add pages.
+  yield db.execute(`INSERT INTO moz_places (url, guid)
+                    VALUES ("http://test1.com/", "test1_______")
+                         , ("http://test2.com/", "test2_______")
+                   `);
+  // Add keywords.
+  yield db.execute(`INSERT INTO moz_keywords (keyword)
+                    VALUES ("kw1")
+                         , ("kw2")
+                         , ("kw3")
+                   `);
+  // Add bookmarks.
+  let now = Date.now() * 1000;
+  let index = 0;
+  yield db.execute(`INSERT INTO moz_bookmarks (type, fk, parent, position, dateAdded, lastModified, keyword_id, guid)
+                    VALUES (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+                             (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark1___")
+                            /* same uri, different keyword */
+                         , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+                             (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark2___")
+                           /* different uri, same keyword as 1 */
+                         , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now},
+                             (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark3___")
+                           /* same uri, same keyword as 1 */
+                         , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+                             (SELECT id FROM moz_keywords WHERE keyword = 'kw1'), "bookmark4___")
+                           /* same uri, same keyword as 2 */
+                         , (1, (SELECT id FROM moz_places WHERE guid = 'test2_______'), 3, ${index++}, ${now}, ${now},
+                             (SELECT id FROM moz_keywords WHERE keyword = 'kw2'), "bookmark5___")
+                           /* different uri, same keyword as 1 */
+                         , (1, (SELECT id FROM moz_places WHERE guid = 'test1_______'), 3, ${index++}, ${now}, ${now},
+                             (SELECT id FROM moz_keywords WHERE keyword = 'kw3'), "bookmark6___")
+                   `);
+  // Add postData.
+  yield db.execute(`INSERT INTO moz_anno_attributes (name)
+                    VALUES ("bookmarkProperties/POSTData")`);
+  yield db.execute(`INSERT INTO moz_items_annos(anno_attribute_id, item_id, content)
+                    VALUES ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+                            (SELECT id FROM moz_bookmarks WHERE guid = "bookmark3___"), "postData1")
+                         , ((SELECT id FROM moz_anno_attributes where name = "bookmarkProperties/POSTData"),
+                            (SELECT id FROM moz_bookmarks WHERE guid = "bookmark5___"), "postData2")`);
+  yield db.close();
+});
+
+add_task(function* database_is_valid() {
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+  let db = yield PlacesUtils.promiseDBConnection();
+  Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_keywords() {
+  // When 2 urls have the same keyword, if one has postData it will be
+  // preferred.
+  let [ url1, postData1 ] = PlacesUtils.getURLAndPostDataForKeyword("kw1");
+  Assert.equal(url1, "http://test2.com/");
+  Assert.equal(postData1, "postData1");
+  let [ url2, postData2 ] = PlacesUtils.getURLAndPostDataForKeyword("kw2");
+  Assert.equal(url2, "http://test2.com/");
+  Assert.equal(postData2, "postData2");
+  let [ url3, postData3 ] = PlacesUtils.getURLAndPostDataForKeyword("kw3");
+  Assert.equal(url3, "http://test1.com/");
+
+  Assert.equal((yield foreign_count("http://test1.com/")), 5); // 4 bookmark2 + 1 keywords
+  Assert.equal((yield foreign_count("http://test2.com/")), 4); // 2 bookmark2 + 2 keywords
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -10,15 +10,17 @@ support-files =
   places_v17.sqlite
   places_v19.sqlite
   places_v21.sqlite
   places_v22.sqlite
   places_v23.sqlite
   places_v24.sqlite
   places_v25.sqlite
   places_v26.sqlite
+  places_v27.sqlite
 
 [test_current_from_downgraded.js]
 [test_current_from_v6.js]
 [test_current_from_v16.js]
 [test_current_from_v19.js]
 [test_current_from_v24.js]
 [test_current_from_v25.js]
+[test_current_from_v26.js]
--- a/toolkit/components/places/tests/queries/test_sorting.js
+++ b/toolkit/components/places/tests/queries/test_sorting.js
@@ -499,17 +499,17 @@ tests.push({
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: PlacesUtils.bookmarks.DEFAULT_INDEX,
         title: "null9",
         keyword: null,
         isInQuery: true },
 
       // if keywords are equal, should fall back to title
       { isBookmark: true,
-        uri: "http://example.com/b2",
+        uri: "http://example.com/b1",
         parentFolder: PlacesUtils.bookmarks.toolbarFolder,
         index: PlacesUtils.bookmarks.DEFAULT_INDEX,
         title: "y8",
         keyword: "b",
         isInQuery: true },
     ];
 
     this._sortedData = [
--- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
@@ -14,66 +14,54 @@
 
 add_task(function* test_keyword_searc() {
   let uri1 = NetUtil.newURI("http://abc/?search=%s");
   let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory");
   yield PlacesTestUtils.addVisits([
     { uri: uri1, title: "Generic page title" },
     { uri: uri2, title: "Generic page title" }
   ]);
-  yield addBookmark({ uri: uri1, title: "Keyword title", keyword: "key"});
+  yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
 
   do_print("Plain keyword query");
   yield check_autocomplete({
     search: "key term",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "Keyword title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "Generic page title", style: ["keyword"] } ]
   });
 
   do_print("Multi-word keyword query");
   yield check_autocomplete({
     search: "key multi word",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "Keyword title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "Generic page title", style: ["keyword"] } ]
   });
 
   do_print("Keyword query with +");
   yield check_autocomplete({
     search: "key blocking+",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "Keyword title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "Generic page title", style: ["keyword"] } ]
   });
 
   do_print("Unescaped term in query");
   yield check_autocomplete({
     search: "key ユニコード",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "Keyword title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "Generic page title", style: ["keyword"] } ]
   });
 
   do_print("Keyword that happens to match a page");
   yield check_autocomplete({
     search: "key ThisPageIsInHistory",
     matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "Generic page title", style: ["bookmark"] } ]
   });
 
   do_print("Keyword without query (without space)");
   yield check_autocomplete({
     search: "key",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Keyword title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Generic page title", style: ["keyword"] } ]
   });
 
   do_print("Keyword without query (with space)");
   yield check_autocomplete({
     search: "key ",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Keyword title", style: ["keyword"] } ]
-  });
-
-  // This adds a second keyword so anything after this will match 2 keywords
-  let uri3 = NetUtil.newURI("http://xyz/?foo=%s");
-  yield PlacesTestUtils.addVisits([ { uri: uri3, title: "Generic page title" } ]);
-  yield addBookmark({ uri: uri3, title: "Keyword title", keyword: "key", style: ["keyword"] });
-
-  do_print("Two keywords matched");
-  yield check_autocomplete({
-    search: "key twoKey",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=twoKey"), title: "Keyword title", style: ["keyword"] },
-               { uri: NetUtil.newURI("http://xyz/?foo=twoKey"), title: "Keyword title", style: ["keyword"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Generic page title", style: ["keyword"] } ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
@@ -14,74 +14,61 @@
 
 add_task(function* test_keyword_search() {
   let uri1 = NetUtil.newURI("http://abc/?search=%s");
   let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory");
   yield PlacesTestUtils.addVisits([
     { uri: uri1, title: "Generic page title" },
     { uri: uri2, title: "Generic page title" }
   ]);
-  yield addBookmark({ uri: uri1, title: "Keyword title", keyword: "key"});
+  yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
 
   do_print("Plain keyword query");
   yield check_autocomplete({
     search: "key term",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Multi-word keyword query");
   yield check_autocomplete({
     search: "key multi word",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword query with +");
   yield check_autocomplete({
     search: "key blocking+",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Unescaped term in query");
   yield check_autocomplete({
     search: "key ユニコード",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword that happens to match a page");
   yield check_autocomplete({
     search: "key ThisPageIsInHistory",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword without query (without space)");
   yield check_autocomplete({
     search: "key",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   do_print("Keyword without query (with space)");
   yield check_autocomplete({
     search: "key ",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "Keyword title", style: [ "action", "keyword" ] } ]
-  });
-
-  // This adds a second keyword so anything after this will match 2 keywords
-  let uri3 = NetUtil.newURI("http://xyz/?foo=%s");
-  yield PlacesTestUtils.addVisits([ { uri: uri3, title: "Generic page title" } ]);
-  yield addBookmark({ uri: uri3, title: "Keyword title", keyword: "key"});
-
-  do_print("Two keywords matched");
-  yield check_autocomplete({
-    search: "key twoKey",
-    searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=twoKey", input: "key twoKey"}), title: "Keyword title", style: [ "action", "keyword" ] },
-               { uri: makeActionURI("keyword", {url: "http://xyz/?foo=twoKey", input: "key twoKey"}), title: "Keyword title", style: [ "action", "keyword" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "Generic page title", style: [ "action", "keyword" ] } ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unit/test_398914.js
+++ b/toolkit/components/places/tests/unit/test_398914.js
@@ -1,150 +1,30 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const bmsvc = PlacesUtils.bookmarks;
-const testFolderId = PlacesUtils.bookmarksMenuFolderId;
-
-// main
 function run_test() {
   var testURI = uri("http://foo.com");
 
   /*
   1. Create a bookmark for a URI, with a keyword and post data.
   2. Create a bookmark for the same URI, with a different keyword and different post data.
   3. Confirm that our method for getting a URI+postdata retains bookmark affinity.
   */
-  var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm1, "foo");
+  var bm1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, testURI, -1, "blah");
+  PlacesUtils.bookmarks.setKeywordForBookmark(bm1, "foo");
   PlacesUtils.setPostDataForBookmark(bm1, "pdata1");
-  var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm2, "bar");
+  var bm2 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId, testURI, -1, "blah");
+  PlacesUtils.bookmarks.setKeywordForBookmark(bm2, "bar");
   PlacesUtils.setPostDataForBookmark(bm2, "pdata2");
 
   // check kw, pd for bookmark 1
   var url, postdata;
   [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
   do_check_eq(testURI.spec, url);
   do_check_eq(postdata, "pdata1");
 
   // check kw, pd for bookmark 2
   [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("bar");
   do_check_eq(testURI.spec, url);
   do_check_eq(postdata, "pdata2");
 
   // cleanup
-  bmsvc.removeItem(bm1);
-  bmsvc.removeItem(bm2);
-
-  /*
-  1. Create two bookmarks with the same URI and keyword.
-  2. Confirm that the most recently created one is returned for that keyword.
-  */
-  var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm1, "foo");
-  PlacesUtils.setPostDataForBookmark(bm1, "pdata1");
-  var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm2, "foo");
-  PlacesUtils.setPostDataForBookmark(bm2, "pdata2");
-
-  var bm1da = bmsvc.getItemDateAdded(bm1);
-  var bm1lm = bmsvc.getItemLastModified(bm1);
-  LOG("bm1 dateAdded: " + bm1da + ", lastModified: " + bm1lm);
-  var bm2da = bmsvc.getItemDateAdded(bm2);
-  var bm2lm = bmsvc.getItemLastModified(bm2);
-  LOG("bm2 dateAdded: " + bm2da + ", lastModified: " + bm2lm);
-  do_check_true(bm1da <= bm2da);
-  do_check_true(bm1lm <= bm2lm);
-
-  [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
-  do_check_eq(testURI.spec, url);
-  do_check_eq(postdata, "pdata2");
-
-  // cleanup
-  bmsvc.removeItem(bm1);
-  bmsvc.removeItem(bm2);
-
-  /*
-  1. Create two bookmarks with the same URI and keyword.
-  2. Modify the first-created bookmark.
-  3. Confirm that the most recently modified one is returned for that keyword.
-  */
-  var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm1, "foo");
-  PlacesUtils.setPostDataForBookmark(bm1, "pdata1");
-  var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm2, "foo");
-  PlacesUtils.setPostDataForBookmark(bm2, "pdata2");
-
-  // modify the older bookmark
-  bmsvc.setItemTitle(bm1, "change");
-
-  var bm1da = bmsvc.getItemDateAdded(bm1);
-  var bm1lm = bmsvc.getItemLastModified(bm1);
-  LOG("bm1 dateAdded: " + bm1da + ", lastModified: " + bm1lm);
-  var bm2da = bmsvc.getItemDateAdded(bm2);
-  var bm2lm = bmsvc.getItemLastModified(bm2);
-  LOG("bm2 dateAdded: " + bm2da + ", lastModified: " + bm2lm);
-  do_check_true(bm1da <= bm2da);
-  // the last modified for bm1 should be at least as big as bm2
-  // but could be equal if the test runs faster than our PRNow()
-  // granularity
-  do_check_true(bm1lm >= bm2lm);
-
-  // we need to ensure that bm1 last modified date is greater
-  // that the modified date of bm2, otherwise in case of a "tie"
-  // bm2 will win, as it has a bigger item id
-  if (bm1lm == bm2lm) 
-    bmsvc.setItemLastModified(bm1, bm2lm + 1000);
-
-  [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
-  do_check_eq(testURI.spec, url);
-  do_check_eq(postdata, "pdata1");
-
-  // cleanup
-  bmsvc.removeItem(bm1);
-  bmsvc.removeItem(bm2);
-
-  /*
-  Test that id breaks ties:
-  1. Create two bookmarks with the same URI and keyword, dateAdded and lastModified.
-  2. Confirm that the most recently created one is returned for that keyword.
-  */
-  var testDate = Date.now() * 1000;
-  var bm1 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm1, "foo");
-  PlacesUtils.setPostDataForBookmark(bm1, "pdata1");
-  bmsvc.setItemDateAdded(bm1, testDate);
-  bmsvc.setItemLastModified(bm1, testDate);
-
-  var bm2 = bmsvc.insertBookmark(testFolderId, testURI, -1, "blah");
-  bmsvc.setKeywordForBookmark(bm2, "foo");
-  PlacesUtils.setPostDataForBookmark(bm2, "pdata2");
-  bmsvc.setItemDateAdded(bm2, testDate);
-  bmsvc.setItemLastModified(bm2, testDate);
-
-  var bm1da = bmsvc.getItemDateAdded(bm1, testDate);
-  var bm1lm = bmsvc.getItemLastModified(bm1);
-  LOG("bm1 dateAdded: " + bm1da + ", lastModified: " + bm1lm);
-  var bm2da = bmsvc.getItemDateAdded(bm2);
-  var bm2lm = bmsvc.getItemLastModified(bm2);
-  LOG("bm2 dateAdded: " + bm2da + ", lastModified: " + bm2lm);
-
-  do_check_eq(bm1da, bm2da);
-  do_check_eq(bm1lm, bm2lm);
-
-
-  var ids = bmsvc.getBookmarkIdsForURI(testURI);
-  do_check_eq(ids[0], bm2);
-  do_check_eq(ids[1], bm1);
-
-  [url, postdata] = PlacesUtils.getURLAndPostDataForKeyword("foo");
-  do_check_eq(testURI.spec, url);
-  do_check_eq(postdata, "pdata2");
-
-  // cleanup
-  bmsvc.removeItem(bm1);
-  bmsvc.removeItem(bm2);
+  PlacesUtils.bookmarks.removeItem(bm1);
+  PlacesUtils.bookmarks.removeItem(bm2);
 }
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -734,21 +734,17 @@ add_task(function* test_add_and_remove_b
   b2_info.guid = yield PT.NewBookmark(b2_info).transact();
   let b2_post_creation_changes = [
    { guid: b2_info.guid
    , isAnnoProperty: true
    , property: ANNO.name
    , newValue: ANNO.value },
    { guid: b2_info.guid
    , property: "keyword"
-   , newValue: KEYWORD },
-   { guid: b2_info.guid
-   , isAnnoProperty: true
-   , property: PlacesUtils.POST_DATA_ANNO
-   , newValue: POST_DATA } ];
+   , newValue: KEYWORD } ];
   ensureItemsChanged(...b2_post_creation_changes);
   ensureTags([TAG_1, TAG_2]);
 
   observer.reset();
   yield PT.undo();
   yield ensureItemsRemoved(b2_info);
   ensureTags([TAG_1]);
 
--- a/toolkit/components/places/tests/unit/test_placesTxn.js
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -716,25 +716,29 @@ add_test(function test_sort_folder_by_na
   run_next_test();
 });
 
 add_test(function test_edit_postData() {
   const POST_DATA_ANNO = "bookmarkProperties/POSTData";
   let postData = "post-test_edit_postData";
   let testURI = NetUtil.newURI("http://test_edit_postData.com");
   let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit Post Data");
-
+  PlacesUtils.bookmarks.setKeywordForBookmark(testBkmId, "kw");
   let txn = new PlacesEditBookmarkPostDataTransaction(testBkmId, postData);
 
   txn.doTransaction();
-  do_check_true(annosvc.itemHasAnnotation(testBkmId, POST_DATA_ANNO));
-  do_check_eq(annosvc.getItemAnnotation(testBkmId, POST_DATA_ANNO), postData);
+  let [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw");
+  Assert.equal(url, testURI.spec);
+  Assert.equal(postData, post_data);
 
   txn.undoTransaction();
-  do_check_false(annosvc.itemHasAnnotation(testBkmId, POST_DATA_ANNO));
+  [url, post_data] = PlacesUtils.getURLAndPostDataForKeyword("kw");
+  Assert.equal(url, testURI.spec);
+  Assert.equal(null, post_data);
+
 
   run_next_test();
 });
 
 add_test(function test_tagURI_untagURI() {
   const TAG_1 = "tag-test_tagURI_untagURI-bar";
   const TAG_2 = "tag-test_tagURI_untagURI-foo";
   let tagURI = NetUtil.newURI("http://test_tagURI_untagURI.com");
--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
@@ -540,63 +540,16 @@ tests.push({
     do_check_true(stmt.executeStep());
     stmt.finalize();
   }
 });
 
 //------------------------------------------------------------------------------
 
 tests.push({
-  name: "D.5",
-  desc: "Fix wrong keywords",
-
-  _validKeywordItemId: null,
-  _invalidKeywordItemId: null,
-  _validKeywordId: 1,
-  _invalidKeywordId: 8888,
-  _placeId: null,
-
-  setup: function() {
-    // Insert a keyword
-    let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword) VALUES(:id, :keyword)");
-    stmt.params["id"] = this._validKeywordId;
-    stmt.params["keyword"] = "used";
-    stmt.execute();
-    stmt.finalize();
-    // Add a place to ensure place_id = 1 is valid
-    this._placeId = addPlace();
-    // Add a bookmark using the keyword
-    this._validKeywordItemId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, bs.unfiledBookmarksFolder, this._validKeywordId);
-    // Add a bookmark using a nonexistent keyword
-    this._invalidKeywordItemId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, bs.unfiledBookmarksFolder, this._invalidKeywordId);
-  },
-
-  check: function() {
-    // Check that item with valid keyword is there
-    let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND keyword_id = :keyword");
-    stmt.params["item_id"] = this._validKeywordItemId;
-    stmt.params["keyword"] = this._validKeywordId;
-    do_check_true(stmt.executeStep());
-    stmt.reset();
-    // Check that item with invalid keyword has been corrected
-    stmt.params["item_id"] = this._invalidKeywordItemId;
-    stmt.params["keyword"] = this._invalidKeywordId;
-    do_check_false(stmt.executeStep());
-    stmt.finalize();
-    // Check that item with invalid keyword has not been removed
-    stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id");
-    stmt.params["item_id"] = this._invalidKeywordItemId;
-    do_check_true(stmt.executeStep());
-    stmt.finalize();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
   name: "D.6",
   desc: "Fix wrong item types | bookmarks",
 
   _separatorId: null,
   _folderId: null,
   _placeId: null,
 
   setup: function() {
@@ -1048,37 +1001,27 @@ tests.push({
   name: "I.1",
   desc: "Remove unused keywords",
 
   _bookmarkId: null,
   _placeId: null,
 
   setup: function() {
     // Insert 2 keywords
-    let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword) VALUES(:id, :keyword)");
+    let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword, place_id) VALUES(:id, :keyword, :place_id)");
     stmt.params["id"] = 1;
-    stmt.params["keyword"] = "used";
-    stmt.execute();
-    stmt.reset();
-    stmt.params["id"] = 2;
     stmt.params["keyword"] = "unused";
+    stmt.params["place_id"] = 100;
     stmt.execute();
     stmt.finalize();
-    // Add a place to ensure place_id = 1 is valid
-    this._placeId = addPlace();
-    // Insert a bookmark using the "used" keyword
-    this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, bs.unfiledBookmarksFolder, 1);
   },
 
   check: function() {
     // Check that "used" keyword is still there
     let stmt = mDBConn.createStatement("SELECT id FROM moz_keywords WHERE keyword = :keyword");
-    stmt.params["keyword"] = "used";
-    do_check_true(stmt.executeStep());
-    stmt.reset();
     // Check that "unused" keyword has gone
     stmt.params["keyword"] = "unused";
     do_check_false(stmt.executeStep());
     stmt.finalize();
   }
 });
 
 
--- a/toolkit/components/places/tests/unit/test_telemetry.js
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -27,17 +27,17 @@ let histograms = {
 function run_test()
 {
   run_next_test();
 }
 
 add_task(function test_execute()
 {
   // Put some trash in the database.
-  const URI = NetUtil.newURI("http://moz.org/");
+  let uri = NetUtil.newURI("http://moz.org/");
 
   let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
                                                     "moz test",
                                                     PlacesUtils.bookmarks.DEFAULT_INDEX);
   let itemId = PlacesUtils.bookmarks.insertBookmark(folderId,
                                                     uri,
                                                     PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                     "moz test");
@@ -59,17 +59,17 @@ add_task(function test_execute()
     .getService(Ci.nsIObserver)
     .observe(null, "gather-telemetry", null);
 
   yield PlacesTestUtils.promiseAsyncUpdates();
 
   // Test expiration probes.
   for (let i = 0; i < 2; i++) {
     yield PlacesTestUtils.addVisits({
-      uri: uri("http://" +  i + ".moz.org/"),
+      uri: NetUtil.newURI("http://" +  i + ".moz.org/"),
       visitDate: Date.now() // [sic]
     });
   }
   Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
   let expire = Cc["@mozilla.org/places/expiration;1"].getService(Ci.nsIObserver);
   expire.observe(null, "places-debug-start-expiration", 1);
   expire.observe(null, "places-debug-start-expiration", -1);