Bug 1434607 - Remove synchronous Bookmarks::setKeywordForBookmark. r=standard8 draft
authorMarco Bonardo <mbonardo@mozilla.com>
Thu, 01 Feb 2018 11:02:41 +0100
changeset 758937 71c53c5e779bf1e121a5efff3d387f2c0ff2abf3
parent 758710 169b1ba48437d5ddc0a4a1828177770b598f7c05
push id100229
push usermak77@bonardo.net
push dateFri, 23 Feb 2018 12:16:58 +0000
reviewersstandard8
bugs1434607
milestone60.0a1
Bug 1434607 - Remove synchronous Bookmarks::setKeywordForBookmark. r=standard8 MozReview-Commit-ID: 3thDN9FIDgm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/nsINavBookmarksService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/tests/bookmarks/test_keywords.js
toolkit/components/places/tests/bookmarks/test_sync_fields.js
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -29,22 +29,20 @@ XPCOMUtils.defineLazyGetter(this, "MOZ_A
 // we really just want "\n". On other platforms, the transferable system
 // converts "\r\n" to "\n".
 const NEWLINE = AppConstants.platform == "macosx" ? "\n" : "\r\n";
 
 // Timers resolution is not always good, it can have a 16ms precision on Win.
 const TIMERS_RESOLUTION_SKEW_MS = 16;
 
 function QI_node(aNode, aIID) {
-  var result = null;
   try {
-    result = aNode.QueryInterface(aIID);
-  } catch (e) {
-  }
-  return result;
+    return aNode.QueryInterface(aIID);
+  } catch (ex) {}
+  return null;
 }
 function asContainer(aNode) {
   return QI_node(aNode, Ci.nsINavHistoryContainerResultNode);
 }
 function asQuery(aNode) {
   return QI_node(aNode, Ci.nsINavHistoryQueryResultNode);
 }
 
@@ -74,18 +72,20 @@ function notify(observers, notification,
  * @param keyword
  *        The keyword to notify, or empty string if a keyword was removed.
  */
 async function notifyKeywordChange(url, keyword, source) {
   // Notify bookmarks about the removal.
   let bookmarks = [];
   await PlacesUtils.bookmarks.fetch({ url }, b => bookmarks.push(b));
   for (let bookmark of bookmarks) {
-    bookmark.id = await PlacesUtils.promiseItemId(bookmark.guid);
-    bookmark.parentId = await PlacesUtils.promiseItemId(bookmark.parentGuid);
+    let ids = await PlacesUtils.promiseManyItemIds([bookmark.guid,
+                                                    bookmark.parentGuid]);
+    bookmark.id = ids.get(bookmark.guid);
+    bookmark.parentId = ids.get(bookmark.parentGuid);
   }
   let observers = PlacesUtils.bookmarks.getObservers();
   for (let bookmark of bookmarks) {
     notify(observers, "onItemChanged", [ bookmark.id, "keyword", false,
                                          keyword,
                                          bookmark.lastModified * 1000,
                                          bookmark.type,
                                          bookmark.parentId,
@@ -189,16 +189,32 @@ function serializeNode(aNode, aIsLivemar
 }
 
 // Imposed to limit database size.
 const DB_URL_LENGTH_MAX = 65536;
 const DB_TITLE_LENGTH_MAX = 4096;
 const DB_DESCRIPTION_LENGTH_MAX = 256;
 
 /**
+ * Executes a boolean validate function, throwing if it returns false.
+ *
+ * @param boolValidateFn
+ *        A boolean validate function.
+ * @return the input value.
+ * @throws if input doesn't pass the validate function.
+ */
+function simpleValidateFunc(boolValidateFn) {
+  return (v, input) => {
+    if (!boolValidateFn(v, input))
+      throw new Error("Invalid value");
+    return v;
+  };
+}
+
+/**
  * List of bookmark object validators, one per each known property.
  * Validators must throw if the property value is invalid and return a fixed up
  * version of the value, if needed.
  */
 const BOOKMARK_VALIDATORS = Object.freeze({
   guid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
   parentGuid: simpleValidateFunc(v => PlacesUtils.isValidGuid(v)),
   guidPrefix: simpleValidateFunc(v => PlacesUtils.isValidGuidPrefix(v)),
@@ -673,19 +689,16 @@ this.PlacesUtils = {
         Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
         while (this._shutdownFunctions.length > 0) {
           this._shutdownFunctions.shift().apply(this);
         }
         break;
     }
   },
 
-  onPageAnnotationSet() {},
-  onPageAnnotationRemoved() {},
-
   /**
    * Determines whether or not a ResultNode is a host container.
    * @param   aNode
    *          A result node
    * @returns true if the node is a host container, false otherwise
    */
   nodeIsHost: function PU_nodeIsHost(aNode) {
     return aNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY &&
@@ -951,17 +964,17 @@ this.PlacesUtils = {
           let titleString = "";
           if (parts.length > i + 1)
             titleString = parts[i + 1];
           else {
             // for drag and drop of files, try to use the leafName as title
             try {
               titleString = Services.io.newURI(uriString).QueryInterface(Ci.nsIURL)
                                 .fileName;
-            } catch (e) {}
+            } catch (ex) {}
           }
           // note:  Services.io.newURI() will throw if uriString is not a valid URI
           if (Services.io.newURI(uriString)) {
             nodes.push({ uri: uriString,
                          title: titleString ? titleString : uriString,
                          type: this.TYPE_X_MOZ_URL });
           }
         }
@@ -1414,75 +1427,66 @@ this.PlacesUtils = {
    * Sets the character-set for a URI.
    *
    * @param {nsIURI} aURI
    * @param {String} aCharset character-set value.
    * @return {Promise}
    */
   setCharsetForURI: function PU_setCharsetForURI(aURI, aCharset) {
     return new Promise(resolve => {
-
       // Delaying to catch issues with asynchronous behavior while waiting
       // to implement asynchronous annotations in bug 699844.
       Services.tm.dispatchToMainThread(function() {
         if (aCharset && aCharset.length > 0) {
           PlacesUtils.annotations.setPageAnnotation(
             aURI, PlacesUtils.CHARSET_ANNO, aCharset, 0,
             Ci.nsIAnnotationService.EXPIRE_NEVER);
         } else {
           PlacesUtils.annotations.removePageAnnotation(
             aURI, PlacesUtils.CHARSET_ANNO);
         }
         resolve();
       });
-
     });
   },
 
   /**
    * Gets the last saved character-set for a URI.
    *
    * @param aURI nsIURI
    * @return {Promise}
    * @resolve a character-set or null.
    */
   getCharsetForURI: function PU_getCharsetForURI(aURI) {
     return new Promise(resolve => {
-
       Services.tm.dispatchToMainThread(function() {
         let charset = null;
-
         try {
           charset = PlacesUtils.annotations.getPageAnnotation(aURI,
                                                               PlacesUtils.CHARSET_ANNO);
         } catch (ex) { }
-
         resolve(charset);
       });
-
     });
   },
 
   /**
    * Gets favicon data for a given page url.
    *
    * @param aPageUrl url of the page to look favicon for.
    * @resolves to an object representing a favicon entry, having the following
    *           properties: { uri, dataLen, data, mimeType }
    * @rejects JavaScript exception if the given url has no associated favicon.
    */
   promiseFaviconData(aPageUrl) {
     return new Promise((resolve, reject) => {
       PlacesUtils.favicons.getFaviconDataForPage(NetUtil.newURI(aPageUrl),
-        function(aURI, aDataLen, aData, aMimeType) {
-          if (aURI) {
-            resolve({ uri: aURI,
-                               dataLen: aDataLen,
-                               data: aData,
-                               mimeType: aMimeType });
+        function(uri, dataLen, data, mimeType) {
+          if (uri) {
+            resolve({ uri, dataLen, data, mimeType });
           } else {
             reject();
           }
         });
     });
   },
 
   /**
@@ -1672,18 +1676,18 @@ this.PlacesUtils = {
       item.typeCode = type;
       if (type == Ci.nsINavBookmarksService.TYPE_BOOKMARK)
         copyProps("charset", "tags", "iconuri");
 
       // Add annotations.
       if (aRow.getResultByName("has_annos")) {
         try {
           item.annos = PlacesUtils.getAnnotationsForItem(itemId);
-        } catch (e) {
-          Cu.reportError("Unexpected error while reading annotations " + e);
+        } catch (ex) {
+          Cu.reportError("Unexpected error while reading annotations " + ex);
         }
       }
 
       switch (type) {
         case Ci.nsINavBookmarksService.TYPE_BOOKMARK:
           item.type = PlacesUtils.TYPE_X_MOZ_PLACE;
           // If this throws due to an invalid url, the item will be skipped.
           item.uri = NetUtil.newURI(aRow.getResultByName("url")).spec;
@@ -1751,17 +1755,16 @@ this.PlacesUtils = {
                JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id
                WHERE place_id = h.id AND n.name = :charset_anno
               ) AS charset
        FROM descendants d
        LEFT JOIN moz_bookmarks b3 ON b3.id = d.parent
        LEFT JOIN moz_places h ON h.id = d.fk
        ORDER BY d.level, d.parent, d.position`;
 
-
     if (!aItemGuid)
       aItemGuid = this.bookmarks.rootGuid;
 
     let hasExcludeItemsCallback =
       aOptions.hasOwnProperty("excludeItemsCallback");
     let excludedParents = new Set();
     let shouldExcludeItem = (aItem, aParentGuid) => {
       let exclude = excludedParents.has(aParentGuid) ||
@@ -2001,18 +2004,23 @@ PlacesUtils.keywords = {
     let hasKeyword = "keyword" in keywordOrEntry;
     let hasUrl = "url" in keywordOrEntry;
 
     if (!hasKeyword && !hasUrl)
       throw new Error("At least keyword or url must be provided");
     if (onResult && typeof onResult != "function")
       throw new Error("onResult callback must be a valid function");
 
-    if (hasUrl)
-      keywordOrEntry.url = new URL(keywordOrEntry.url);
+    if (hasUrl) {
+      try {
+        keywordOrEntry.url = BOOKMARK_VALIDATORS.url(keywordOrEntry.url);
+      } catch (ex) {
+        throw new Error(keywordOrEntry.url + " is not a valid URL");
+      }
+    }
     if (hasKeyword)
       keywordOrEntry.keyword = keywordOrEntry.keyword.trim().toLowerCase();
 
     let safeOnResult = entry => {
       if (onResult) {
         try {
           onResult(entry);
         } catch (ex) {
@@ -2047,17 +2055,17 @@ PlacesUtils.keywords = {
 
   /**
    * Adds a new keyword and postData for the given URL.
    *
    * @param keywordEntry
    *        An object describing the keyword to insert, in the form:
    *        {
    *          keyword: non-empty string,
-   *          URL: URL or href to associate to the keyword,
+   *          url: URL or href to associate to the keyword,
    *          postData: optional POST data to associate to the keyword
    *          source: The change source, forwarded to all bookmark observers.
    *            Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
    *        }
    * @note Do not define a postData property if there isn't any POST data.
    *       Defining an empty string for POST data is equivalent to not having it.
    * @resolves when the addition is complete.
    */
@@ -2076,17 +2084,21 @@ PlacesUtils.keywords = {
 
     if (!("source" in keywordEntry)) {
       keywordEntry.source = PlacesUtils.bookmarks.SOURCES.DEFAULT;
     }
     let { keyword, url, source } = keywordEntry;
     keyword = keyword.trim().toLowerCase();
     let postData = keywordEntry.postData || "";
     // This also checks href for validity
-    url = new URL(url);
+    try {
+      url = BOOKMARK_VALIDATORS.url(url);
+    } catch (ex) {
+      throw new Error(url + " is not a valid URL");
+    }
 
     return PlacesUtils.withConnectionWrapper("PlacesUtils.keywords.insert", async db => {
         let cache = await promiseKeywordsCache();
 
         // Trying to set the same keyword is a no-op.
         let oldEntry = cache.get(keyword);
         if (oldEntry && oldEntry.url.href == url.href &&
             (oldEntry.postData || "") == postData) {
@@ -2561,24 +2573,8 @@ var GuidHelper = {
       };
       PlacesUtils.bookmarks.addObserver(this.observer);
       PlacesUtils.registerShutdownFunction(() => {
         PlacesUtils.bookmarks.removeObserver(this.observer);
       });
     }
   }
 };
-
-/**
- * Executes a boolean validate function, throwing if it returns false.
- *
- * @param boolValidateFn
- *        A boolean validate function.
- * @return the input value.
- * @throws if input doesn't pass the validate function.
- */
-function simpleValidateFunc(boolValidateFn) {
-  return (v, input) => {
-    if (!boolValidateFn(v, input))
-      throw new Error("Invalid value");
-    return v;
-  };
-}
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -532,34 +532,23 @@ interface nsINavBookmarksService : nsISu
   /**
 
   /**
    * Get the parent folder's id for an item.
    */
   long long getFolderIdForItem(in long long aItemId);
 
   /**
-   * 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.
-   *
-   * @deprecated Use PlacesUtils.keywords.insert() API instead.
-   */
-  void setKeywordForBookmark(in long long aItemId,
-                             in AString aKeyword,
-                             [optional] in unsigned short aSource);
-
-  /**
    * Retrieves the keyword for the given bookmark. Will be void string
    * (null in JS) if no such keyword is found.
    *
    * @deprecated Use PlacesUtils.keywords.fetch() API instead.
    */
   AString getKeywordForBookmark(in long long aItemId);
+
   /**
    * Adds a bookmark observer. If ownsWeak is false, the bookmark service will
    * keep an owning reference to the observer.  If ownsWeak is true, then
    * aObserver must implement nsISupportsWeakReference, and the bookmark
    * service will keep a weak reference to the observer.
    */
   void addObserver(in nsINavBookmarkObserver observer,
                    [optional] in boolean ownsWeak);
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -2245,275 +2245,16 @@ nsNavBookmarks::GetBookmarksForURI(nsIUR
   }
 
   return NS_OK;
 }
 
 
 
 NS_IMETHODIMP
-nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
-                                      const nsAString& aUserCasedKeyword,
-                                      uint16_t aSource)
-{
-  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);
-  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);
-
-  // 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;
-  }
-
-  int64_t syncChangeDelta = DetermineSyncChangeDelta(aSource);
-
-  mozStorageTransaction transaction(mDB->MainConn(), false);
-
-  // Remove the existing keywords.
-  // Note this is wrong because in the insert-new-keyword case we should only
-  // remove those having the same POST data, but this API doesn't allow that.
-  // And in any case, this API is going away.
-  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);
-  }
-
-  if (keyword.IsEmpty()) {
-    // 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);
-
-    if (syncChangeDelta && !bookmarks.IsEmpty()) {
-      // Build a query to update all bookmarks in a single statement.
-      nsAutoCString changedIds;
-      changedIds.AppendInt(bookmarks[0].id);
-      for (uint32_t i = 1; i < bookmarks.Length(); ++i) {
-        changedIds.Append(',');
-        changedIds.AppendInt(bookmarks[i].id);
-      }
-      // Update the sync change counter for all bookmarks with the removed
-      // keyword.
-      nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
-        NS_LITERAL_CSTRING(
-        "UPDATE moz_bookmarks SET "
-         "syncChangeCounter = syncChangeCounter + :delta "
-        "WHERE id IN (") + changedIds + NS_LITERAL_CSTRING(")")
-      );
-      NS_ENSURE_STATE(stmt);
-      mozStorageStatementScoper scoper(stmt);
-
-      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("delta"),
-                                 syncChangeDelta);
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      rv = stmt->Execute();
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-
-    rv = transaction.Commit();
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
-      NOTIFY_OBSERVERS(mCanNotify, 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,
-                                     // Abusing oldVal to pass out the url.
-                                     bookmark.url,
-                                     aSource));
-    }
-
-    return NS_OK;
-  }
-
-  // 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.
-  nsAutoCString oldSpec;
-  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) {
-      rv = stmt->GetUTF8String(0, oldSpec);
-      NS_ENSURE_SUCCESS(rv, rv);
-      rv = NS_NewURI(getter_AddRefs(oldUri), oldSpec);
-      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, 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,
-                                     // Abusing oldVal to pass out the url.
-                                     oldSpec,
-                                     aSource));
-    }
-
-    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, post_data) "
-      "VALUES (:keyword, :place_id, '')"
-    );
-  }
-  NS_ENSURE_STATE(stmt);
-  mozStorageStatementScoper scoper(stmt);
-
-  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);
-
-  nsTArray<BookmarkData> bookmarks;
-  rv = GetBookmarksForURI(uri, bookmarks);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (syncChangeDelta && !bookmarks.IsEmpty()) {
-    // Build a query to update all bookmarks in a single statement.
-    nsAutoCString changedIds;
-    changedIds.AppendInt(bookmarks[0].id);
-    for (uint32_t i = 1; i < bookmarks.Length(); ++i) {
-      changedIds.Append(',');
-      changedIds.AppendInt(bookmarks[i].id);
-    }
-    // Update the sync change counter for all bookmarks with the new keyword.
-    nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
-      NS_LITERAL_CSTRING(
-      "UPDATE moz_bookmarks SET "
-       "syncChangeCounter = syncChangeCounter + :delta "
-      "WHERE id IN (") + changedIds + NS_LITERAL_CSTRING(")")
-    );
-    NS_ENSURE_STATE(stmt);
-    mozStorageStatementScoper scoper(stmt);
-
-    rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("delta"), syncChangeDelta);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = stmt->Execute();
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  rv = transaction.Commit();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // In both cases, notify about the change.
-  for (uint32_t i = 0; i < bookmarks.Length(); ++i) {
-    NOTIFY_OBSERVERS(mCanNotify, 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,
-                                   // Abusing oldVal to pass out the url.
-                                   bookmark.url,
-                                   aSource));
-  }
-
-  return NS_OK;
-}
-
-
-NS_IMETHODIMP
 nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword)
 {
   NS_ENSURE_ARG_MIN(aBookmarkId, 1);
   aKeyword.Truncate(0);
 
   // 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(
--- a/toolkit/components/places/tests/bookmarks/test_keywords.js
+++ b/toolkit/components/places/tests/bookmarks/test_keywords.js
@@ -74,20 +74,16 @@ function expectNotifications() {
   return observer;
 }
 
 add_task(function test_invalid_input() {
   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(async function test_addBookmarkAndKeyword() {
   let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
   Preferences.set("privacy.reduceTimerPrecision", false);
 
   registerCleanupFunction(function() {
     Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
@@ -549,18 +545,16 @@ add_task(async function test_invalidatio
   info("Remove bookmark with keyword");
   await PlacesUtils.bookmarks.remove(tb.guid);
 
   ok(!(await PlacesUtils.keywords.fetch({ url: "http://getthunderbird.com" })),
     "Should not return keywords for removed bookmark URL");
 
   ok(!(await PlacesUtils.keywords.fetch({ keyword: "tb" })),
     "Should not return URL for removed bookmark keyword");
-
-  await PlacesTestUtils.promiseAsyncUpdates();
   await check_orphans();
 
   await PlacesUtils.bookmarks.eraseEverything();
 });
 
 add_task(async function test_eraseAllBookmarks() {
   info("Insert bookmarks");
   let fx = await PlacesUtils.bookmarks.insert({
--- a/toolkit/components/places/tests/bookmarks/test_sync_fields.js
+++ b/toolkit/components/places/tests/bookmarks/test_sync_fields.js
@@ -112,23 +112,26 @@ class TestCases {
     info(`Set anno on ${guid}`);
     await checkSyncFields(guid, { syncChangeCounter: 3 });
 
     // Tagging a bookmark should update its change counter.
     await this.tagURI(testUri, ["test-tag"]);
     info(`Tagged bookmark ${guid}`);
     await checkSyncFields(guid, { syncChangeCounter: 4 });
 
-    await this.setKeyword(guid, "keyword");
-    info(`Set keyword for bookmark ${guid}`);
-    await checkSyncFields(guid, { syncChangeCounter: 5 });
-
-    await this.removeKeyword(guid, "keyword");
-    info(`Removed keyword from bookmark ${guid}`);
-    await checkSyncFields(guid, { syncChangeCounter: 6 });
+    if ("setKeyword" in this) {
+      await this.setKeyword(guid, "keyword");
+      info(`Set keyword for bookmark ${guid}`);
+      await checkSyncFields(guid, { syncChangeCounter: 5 });
+    }
+    if ("removeKeyword" in this) {
+      await this.removeKeyword(guid, "keyword");
+      info(`Removed keyword from bookmark ${guid}`);
+      await checkSyncFields(guid, { syncChangeCounter: 6 });
+    }
   }
 
   async testSeparators() {
     let insertSyncedBookmark = uri => {
       return this.insertBookmark(PlacesUtils.bookmarks.unfiledGuid,
                                  NetUtil.newURI(uri),
                                  PlacesUtils.bookmarks.DEFAULT_INDEX,
                                  "A bookmark name");
@@ -279,29 +282,16 @@ class SyncTestCases extends TestCases {
     PlacesUtils.bookmarks.removeItem(id);
   }
 
   async setTitle(guid, title) {
     let id = await PlacesUtils.promiseItemId(guid);
     PlacesUtils.bookmarks.setItemTitle(id, title);
   }
 
-  async setKeyword(guid, keyword) {
-    let id = await PlacesUtils.promiseItemId(guid);
-    PlacesUtils.bookmarks.setKeywordForBookmark(id, keyword);
-  }
-
-  async removeKeyword(guid, keyword) {
-    let id = await PlacesUtils.promiseItemId(guid);
-    if (PlacesUtils.bookmarks.getKeywordForBookmark(id) != keyword) {
-      throw new Error(`Keyword ${keyword} not set for bookmark ${guid}`);
-    }
-    PlacesUtils.bookmarks.setKeywordForBookmark(id, "");
-  }
-
   async tagURI(uri, tags) {
     PlacesUtils.tagging.tagURI(uri, tags);
   }
 
   async reorder(parentGuid, childGuids) {
     let parentId = await PlacesUtils.promiseItemId(parentGuid);
     for (let index = 0; index < childGuids.length; ++index) {
       let id = await PlacesUtils.promiseItemId(childGuids[index]);