Bug 1382363 - Change most uses of `promiseDBConnection` to `withConnectionWrapper` in `PlacesSyncUtils`. r=markh
authorKit Cambridge <kit@yakshaving.ninja>
Tue, 25 Jul 2017 11:53:45 -0700
changeset 419766 43a99fa7cf0291c08a2d2baca422161d8d8ab27f
parent 419765 6061a480a9b194150791f9bf0708da9ac94c3a7b
child 419767 4e1a1565e4175024b713b04bf20cef1fe34b4cb2
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1382363
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1382363 - Change most uses of `promiseDBConnection` to `withConnectionWrapper` in `PlacesSyncUtils`. r=markh The read-only `promiseDBConnection` can return stale data if writes are still pending. This patch changes `PlacesSyncUtils` to use the read-write `withConnectionWrapper` instead, ensuring we fetch up-to-date info. I didn't change `fetchURLFrecency` and `fetchGuidsWithAnno`. `fetchURLFrecency` is only used to set the sort index for history records, and frecency is recalculated in the background, anyway. `fetchGuidsWithAnno` is only used in tests. MozReview-Commit-ID: Gq16sNm7K2e
services/sync/tests/unit/test_bookmark_tracker.js
toolkit/components/places/PlacesSyncUtils.jsm
toolkit/components/places/tests/unit/test_sync_utils.js
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -1,16 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const {
-  // `fetchGuidsWithAnno` isn't exported, but we can still access it here via a
-  // backstage pass.
-  fetchGuidsWithAnno,
-} = Cu.import("resource://gre/modules/PlacesSyncUtils.jsm", {});
 Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://testing-common/PlacesTestUtils.jsm");
@@ -356,27 +351,25 @@ add_task(async function test_batch_track
 
   await startTracking();
 
   PlacesUtils.bookmarks.runInBatchMode({
     runBatched() {
       PlacesUtils.bookmarks.createFolder(
         PlacesUtils.bookmarks.bookmarksMenuFolder,
         "Test Folder", PlacesUtils.bookmarks.DEFAULT_INDEX);
-      // `runBatched` runs within a transaction that commits when
-      // `runInBatchMode` returns. Since `promiseChangedIDs` uses a read-only
-      // connection to fetch changes, it won't see the folder or its parent
-      // until we're out of batch mode.
-      Async.promiseSpinningly(verifyTrackedCount(0));
+      // We should be tracking the new folder and its parent (and need to jump
+      // through blocking hoops...)
+      Async.promiseSpinningly(verifyTrackedCount(2));
+      // But not have bumped the score.
       do_check_eq(tracker.score, 0);
     }
   }, null);
 
-  // Out of batch mode - now we should be tracking the new folder and its
-  // parent, and the score should be up.
+  // Out of batch mode - tracker should be the same, but score should be up.
   await verifyTrackedCount(2);
   do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
   await cleanup();
 });
 
 add_task(async function test_nested_batch_tracking() {
   _("Test tracker does the correct thing if a places 'batch' is nested");
 
@@ -385,29 +378,31 @@ add_task(async function test_nested_batc
   PlacesUtils.bookmarks.runInBatchMode({
     runBatched() {
 
       PlacesUtils.bookmarks.runInBatchMode({
         runBatched() {
           PlacesUtils.bookmarks.createFolder(
             PlacesUtils.bookmarks.bookmarksMenuFolder,
             "Test Folder", PlacesUtils.bookmarks.DEFAULT_INDEX);
-          Async.promiseSpinningly(verifyTrackedCount(0));
+          // We should be tracking the new folder and its parent (and need to jump
+          // through blocking hoops...)
+          Async.promiseSpinningly(verifyTrackedCount(2));
+          // But not have bumped the score.
           do_check_eq(tracker.score, 0);
         }
       }, null);
       _("inner batch complete.");
       // should still not have a score as the outer batch is pending.
-      Async.promiseSpinningly(verifyTrackedCount(0));
+      Async.promiseSpinningly(verifyTrackedCount(2));
       do_check_eq(tracker.score, 0);
     }
   }, null);
 
-  // Out of both batches - now we should be tracking the new folder and its
-  // parent, and the score should be up.
+  // Out of both batches - tracker should be the same, but score should be up.
   await verifyTrackedCount(2);
   do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
   await cleanup();
 });
 
 add_task(async function test_tracker_sql_batching() {
   _("Test tracker does the correct thing when it is forced to batch SQL queries");
 
@@ -1703,35 +1698,35 @@ add_task(async function test_mobile_quer
 
   try {
     await startTracking();
 
     // Creates the organizer queries as a side effect.
     let leftPaneId = PlacesUIUtils.leftPaneFolderId;
     _(`Left pane root ID: ${leftPaneId}`);
 
-    let allBookmarksGuids = await fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
-                                                     "AllBookmarks");
+    let allBookmarksGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      "PlacesOrganizer/OrganizerQuery", "AllBookmarks");
     equal(allBookmarksGuids.length, 1, "Should create folder with all bookmarks queries");
     let allBookmarkGuid = allBookmarksGuids[0];
 
     _("Try creating query after organizer is ready");
     tracker._ensureMobileQuery();
-    let queryGuids = await fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
-                                              "MobileBookmarks");
+    let queryGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      "PlacesOrganizer/OrganizerQuery", "MobileBookmarks");
     equal(queryGuids.length, 0, "Should not create query without any mobile bookmarks");
 
     _("Insert mobile bookmark, then create query");
     let mozBmk = await PlacesUtils.bookmarks.insert({
       parentGuid: PlacesUtils.bookmarks.mobileGuid,
       url: "https://mozilla.org",
     });
     tracker._ensureMobileQuery();
-    queryGuids = await fetchGuidsWithAnno("PlacesOrganizer/OrganizerQuery",
-                                          "MobileBookmarks");
+    queryGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      "PlacesOrganizer/OrganizerQuery", "MobileBookmarks");
     equal(queryGuids.length, 1, "Should create query once mobile bookmarks exist");
 
     let queryGuid = queryGuids[0];
 
     let queryInfo = await PlacesUtils.bookmarks.fetch(queryGuid);
     equal(queryInfo.url, `place:folder=${PlacesUtils.mobileFolderId}`, "Query should point to mobile root");
     equal(queryInfo.title, "Mobile Bookmarks", "Query title should be localized");
     equal(queryInfo.parentGuid, allBookmarkGuid, "Should append mobile query to all bookmarks queries");
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -104,80 +104,76 @@ const BookmarkSyncUtils = PlacesSyncUtil
   /**
    * Converts a Sync record ID to a Places GUID.
    */
   syncIdToGuid(syncId) {
     return ROOT_SYNC_ID_TO_GUID[syncId] || syncId;
   },
 
   /**
-   * Resolves to an array of the syncIDs of bookmarks that have a nonzero change
-   * counter
-   */
-  async getChangedIds() {
-    let db = await PlacesUtils.promiseDBConnection();
-    let changes = await pullSyncChanges(db);
-    return Object.keys(changes);
-  },
-
-  /**
    * Fetches the sync IDs for a folder's children, ordered by their position
    * within the folder.
    */
-  async fetchChildSyncIds(parentSyncId) {
+  fetchChildSyncIds(parentSyncId) {
     PlacesUtils.SYNC_BOOKMARK_VALIDATORS.syncId(parentSyncId);
     let parentGuid = BookmarkSyncUtils.syncIdToGuid(parentSyncId);
 
-    let db = await PlacesUtils.promiseDBConnection();
-    let childGuids = await fetchChildGuids(db, parentGuid);
-    return childGuids.map(guid =>
-      BookmarkSyncUtils.guidToSyncId(guid)
+    return PlacesUtils.withConnectionWrapper(
+      "BookmarkSyncUtils: fetchChildSyncIds", async function(db) {
+        let childGuids = await fetchChildGuids(db, parentGuid);
+        return childGuids.map(guid =>
+          BookmarkSyncUtils.guidToSyncId(guid)
+        );
+      }
     );
   },
 
   /**
    * Returns an array of `{ syncId, syncable }` tuples for all items in
    * `requestedSyncIds`. If any requested ID is a folder, all its descendants
    * will be included. Ancestors of non-syncable items are not included; if
    * any are missing on the server, the requesting client will need to make
    * another repair request.
    *
    * Sync calls this method to respond to incoming bookmark repair requests
    * and upload items that are missing on the server.
    */
-  async fetchSyncIdsForRepair(requestedSyncIds) {
+  fetchSyncIdsForRepair(requestedSyncIds) {
     let requestedGuids = requestedSyncIds.map(BookmarkSyncUtils.syncIdToGuid);
-    let db = await PlacesUtils.promiseDBConnection();
-    let rows = await db.executeCached(`
-      WITH RECURSIVE
-      syncedItems(id) AS (
-        SELECT b.id FROM moz_bookmarks b
-        WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
-                         'mobile______')
-        UNION ALL
-        SELECT b.id FROM moz_bookmarks b
-        JOIN syncedItems s ON b.parent = s.id
-      ),
-      descendants(id) AS (
-        SELECT b.id FROM moz_bookmarks b
-        WHERE b.guid IN (${requestedGuids.map(guid => JSON.stringify(guid)).join(",")})
-        UNION ALL
-        SELECT b.id FROM moz_bookmarks b
-        JOIN descendants d ON d.id = b.parent
-      )
-      SELECT b.guid, s.id NOT NULL AS syncable
-      FROM descendants d
-      JOIN moz_bookmarks b ON b.id = d.id
-      LEFT JOIN syncedItems s ON s.id = d.id
-      `);
-    return rows.map(row => {
-      let syncId = BookmarkSyncUtils.guidToSyncId(row.getResultByName("guid"));
-      let syncable = !!row.getResultByName("syncable");
-      return { syncId, syncable };
-    });
+    return PlacesUtils.withConnectionWrapper(
+      "BookmarkSyncUtils: fetchSyncIdsForRepair", async function(db) {
+        let rows = await db.executeCached(`
+          WITH RECURSIVE
+          syncedItems(id) AS (
+            SELECT b.id FROM moz_bookmarks b
+            WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
+                             'mobile______')
+            UNION ALL
+            SELECT b.id FROM moz_bookmarks b
+            JOIN syncedItems s ON b.parent = s.id
+          ),
+          descendants(id) AS (
+            SELECT b.id FROM moz_bookmarks b
+            WHERE b.guid IN (${requestedGuids.map(guid => JSON.stringify(guid)).join(",")})
+            UNION ALL
+            SELECT b.id FROM moz_bookmarks b
+            JOIN descendants d ON d.id = b.parent
+          )
+          SELECT b.guid, s.id NOT NULL AS syncable
+          FROM descendants d
+          JOIN moz_bookmarks b ON b.id = d.id
+          LEFT JOIN syncedItems s ON s.id = d.id
+          `);
+        return rows.map(row => {
+          let syncId = BookmarkSyncUtils.guidToSyncId(row.getResultByName("guid"));
+          let syncable = !!row.getResultByName("syncable");
+          return { syncId, syncable };
+        });
+      }
+    );
   },
 
   /**
    * Migrates an array of `{ syncId, modified }` tuples from the old JSON-based
    * tracker to the new sync change counter. `modified` is when the change was
    * added to the old tracker, in milliseconds.
    *
    * Sync calls this method before the first bookmark sync after the Places
@@ -292,52 +288,55 @@ const BookmarkSyncUtils = PlacesSyncUtil
     let orderedChildrenGuids = childSyncIds.map(BookmarkSyncUtils.syncIdToGuid);
     return PlacesUtils.bookmarks.reorder(parentGuid, orderedChildrenGuids,
                                          { source: SOURCE_SYNC });
   },
 
   /**
    * Resolves to true if there are known sync changes.
    */
-  async havePendingChanges() {
-    let db = await PlacesUtils.promiseDBConnection();
-    let rows = await db.executeCached(`
-      WITH RECURSIVE
-      syncedItems(id, guid, syncChangeCounter) AS (
-        SELECT b.id, b.guid, b.syncChangeCounter
-         FROM moz_bookmarks b
-         WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
-                          'mobile______')
-        UNION ALL
-        SELECT b.id, b.guid, b.syncChangeCounter
-        FROM moz_bookmarks b
-        JOIN syncedItems s ON b.parent = s.id
-      ),
-      changedItems(guid) AS (
-        SELECT guid FROM syncedItems
-        WHERE syncChangeCounter >= 1
-        UNION ALL
-        SELECT guid FROM moz_bookmarks_deleted
-      )
-      SELECT EXISTS(SELECT guid FROM changedItems) AS haveChanges`);
-    return !!rows[0].getResultByName("haveChanges");
+  havePendingChanges() {
+    return PlacesUtils.withConnectionWrapper(
+      "BookmarkSyncUtils: havePendingChanges", async function(db) {
+        let rows = await db.executeCached(`
+          WITH RECURSIVE
+          syncedItems(id, guid, syncChangeCounter) AS (
+            SELECT b.id, b.guid, b.syncChangeCounter
+             FROM moz_bookmarks b
+             WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
+                              'mobile______')
+            UNION ALL
+            SELECT b.id, b.guid, b.syncChangeCounter
+            FROM moz_bookmarks b
+            JOIN syncedItems s ON b.parent = s.id
+          ),
+          changedItems(guid) AS (
+            SELECT guid FROM syncedItems
+            WHERE syncChangeCounter >= 1
+            UNION ALL
+            SELECT guid FROM moz_bookmarks_deleted
+          )
+          SELECT EXISTS(SELECT guid FROM changedItems) AS haveChanges`);
+        return !!rows[0].getResultByName("haveChanges");
+      }
+    );
   },
 
   /**
    * Returns a changeset containing local bookmark changes since the last sync.
    *
    * @return {Promise} resolved once all items have been fetched.
    * @resolves to an object containing records for changed bookmarks, keyed by
    *           the sync ID.
    * @see pullSyncChanges for the implementation, and markChangesAsSyncing for
    *      an explanation of why we update the sync status.
    */
-  async pullChanges() {
-    let db = await PlacesUtils.promiseDBConnection();
-    return pullSyncChanges(db);
+  pullChanges() {
+    return PlacesUtils.withConnectionWrapper(
+      "BookmarkSyncUtils: pullChanges", pullSyncChanges);
   },
 
   /**
    * Updates the sync status of all "NEW" bookmarks to "NORMAL", so that Sync
    * can recover correctly after an interrupted sync.
    *
    * @param changeRecords
    *        A changeset containing sync change records, as returned by
@@ -437,62 +436,65 @@ const BookmarkSyncUtils = PlacesSyncUtil
    *  3. Remove the tombstoned folder. Because we don't do this in a
    *     transaction, the user might move new items into the folder before we
    *     can remove it. In that case, we keep the folder and upload the new
    *     subtree to the server.
    *
    * See the comment above `BookmarksStore::deletePending` for the details on
    * why delete works the way it does.
    */
-  async remove(syncIds) {
+  remove(syncIds) {
     if (!syncIds.length) {
       return null;
     }
 
-    let folderGuids = [];
-    for (let syncId of syncIds) {
-      if (syncId in ROOT_SYNC_ID_TO_GUID) {
-        BookmarkSyncLog.warn(`remove: Refusing to remove root ${syncId}`);
-        continue;
-      }
-      let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
-      let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
-      if (!bookmarkItem) {
-        BookmarkSyncLog.trace(`remove: Item ${guid} already removed`);
-        continue;
-      }
-      let kind = await getKindForItem(bookmarkItem);
-      if (kind == BookmarkSyncUtils.KINDS.FOLDER) {
-        folderGuids.push(bookmarkItem.guid);
-        continue;
+    return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: remove",
+      async function(db) {
+        let folderGuids = [];
+        for (let syncId of syncIds) {
+          if (syncId in ROOT_SYNC_ID_TO_GUID) {
+            BookmarkSyncLog.warn(`remove: Refusing to remove root ${syncId}`);
+            continue;
+          }
+          let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
+          let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
+          if (!bookmarkItem) {
+            BookmarkSyncLog.trace(`remove: Item ${guid} already removed`);
+            continue;
+          }
+          let kind = await getKindForItem(db, bookmarkItem);
+          if (kind == BookmarkSyncUtils.KINDS.FOLDER) {
+            folderGuids.push(bookmarkItem.guid);
+            continue;
+          }
+          let wasRemoved = await deleteSyncedAtom(bookmarkItem);
+          if (wasRemoved) {
+             BookmarkSyncLog.trace(`remove: Removed item ${guid} with ` +
+                                   `kind ${kind}`);
+          }
+        }
+
+        for (let guid of folderGuids) {
+          let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
+          if (!bookmarkItem) {
+            BookmarkSyncLog.trace(`remove: Folder ${guid} already removed`);
+            continue;
+          }
+          let wasRemoved = await deleteSyncedFolder(db, bookmarkItem);
+          if (wasRemoved) {
+            BookmarkSyncLog.trace(`remove: Removed folder ${bookmarkItem.guid}`);
+          }
+        }
+
+        // TODO (Bug 1313890): Refactor the bookmarks engine to pull change records
+        // before uploading, instead of returning records to merge into the engine's
+        // initial changeset.
+        return pullSyncChanges(db);
       }
-      let wasRemoved = await deleteSyncedAtom(bookmarkItem);
-      if (wasRemoved) {
-         BookmarkSyncLog.trace(`remove: Removed item ${guid} with ` +
-                               `kind ${kind}`);
-      }
-    }
-
-    for (let guid of folderGuids) {
-      let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
-      if (!bookmarkItem) {
-        BookmarkSyncLog.trace(`remove: Folder ${guid} already removed`);
-        continue;
-      }
-      let wasRemoved = await deleteSyncedFolder(bookmarkItem);
-      if (wasRemoved) {
-        BookmarkSyncLog.trace(`remove: Removed folder ${bookmarkItem.guid}`);
-      }
-    }
-
-    // TODO (Bug 1313890): Refactor the bookmarks engine to pull change records
-    // before uploading, instead of returning records to merge into the engine's
-    // initial changeset.
-    let db = await PlacesUtils.promiseDBConnection();
-    return pullSyncChanges(db);
+    );
   },
 
   /**
    * Increments the change counter of a non-folder item and its parent. Sync
    * calls this method to override a remote deletion for an item that's changed
    * locally.
    *
    * @param syncId
@@ -506,29 +508,32 @@ const BookmarkSyncUtils = PlacesSyncUtil
   async touch(syncId) {
     PlacesUtils.SYNC_BOOKMARK_VALIDATORS.syncId(syncId);
     let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
 
     let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
     if (!bookmarkItem) {
       return null;
     }
-    let kind = await getKindForItem(bookmarkItem);
-    if (kind == BookmarkSyncUtils.KINDS.FOLDER) {
-      // We avoid reviving folders since reviving them properly would require
-      // reviving their children as well. Unfortunately, this is the wrong
-      // choice in the case of a bookmark restore where the bookmarks engine
-      // fails to wipe the server. In that case, if the server has the folder
-      // as deleted, we *would* want to reupload this folder. This is mitigated
-      // by the fact that `remove` moves any undeleted children to the
-      // grandparent when deleting the parent.
-      return null;
-    }
     return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: touch",
-      db => touchSyncBookmark(db, bookmarkItem));
+      async function(db) {
+        let kind = await getKindForItem(db, bookmarkItem);
+        if (kind == BookmarkSyncUtils.KINDS.FOLDER) {
+          // We avoid reviving folders since reviving them properly would require
+          // reviving their children as well. Unfortunately, this is the wrong
+          // choice in the case of a bookmark restore where the bookmarks engine
+          // fails to wipe the server. In that case, if the server has the folder
+          // as deleted, we *would* want to reupload this folder. This is mitigated
+          // by the fact that `remove` moves any undeleted children to the
+          // grandparent when deleting the parent.
+          return null;
+        }
+        return touchSyncBookmark(db, bookmarkItem);
+      }
+    );
   },
 
   /**
    * Returns true for sync IDs that are considered roots.
    */
   isRootSyncID(syncID) {
     return ROOT_SYNC_ID_TO_GUID.hasOwnProperty(syncID);
   },
@@ -637,17 +642,18 @@ const BookmarkSyncUtils = PlacesSyncUtil
    * @resolves to an object representing the updated bookmark.
    * @rejects if it's not possible to update the given bookmark.
    * @throws if the arguments are invalid.
    */
   update(info) {
     let updateInfo = validateSyncBookmarkObject("BookmarkSyncUtils: update",
       info, { syncId: { required: true } });
 
-    return updateSyncBookmark(updateInfo);
+    return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: update",
+      db => updateSyncBookmark(db, updateInfo));
   },
 
   /**
    * Inserts a synced bookmark into the tree. Only Sync should call this
    * method; other callers should use `Bookmarks.insert`.
    *
    * The following properties are supported:
    *  - kind: Required.
@@ -668,17 +674,19 @@ const BookmarkSyncUtils = PlacesSyncUtil
    *
    * @return {Promise} resolved when the creation is complete.
    * @resolves to an object representing the created bookmark.
    * @rejects if it's not possible to create the requested bookmark.
    * @throws if the arguments are invalid.
    */
   insert(info) {
     let insertInfo = validateNewBookmark("BookmarkSyncUtils: insert", info);
-    return insertSyncBookmark(insertInfo);
+
+    return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: insert",
+      db => insertSyncBookmark(db, insertInfo));
   },
 
   /**
    * Fetches a Sync bookmark object for an item in the tree. The object contains
    * the following properties, depending on the item's kind:
    *
    *  - kind (all): A string representing the item's kind.
    *  - syncId (all): The item's sync ID.
@@ -707,80 +715,60 @@ const BookmarkSyncUtils = PlacesSyncUtil
    *  - index ("separator"): The separator's position within its parent.
    */
   async fetch(syncId) {
     let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
     let bookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
     if (!bookmarkItem) {
       return null;
     }
-
-    // Convert the Places bookmark object to a Sync bookmark and add
-    // kind-specific properties. Titles are required for bookmarks,
-    // folders, and livemarks; optional for queries, and omitted for
-    // separators.
-    let kind = await getKindForItem(bookmarkItem);
-    let item;
-    switch (kind) {
-      case BookmarkSyncUtils.KINDS.BOOKMARK:
-        item = await fetchBookmarkItem(bookmarkItem);
-        break;
+    return PlacesUtils.withConnectionWrapper("BookmarkSyncUtils: fetch",
+      async function(db) {
+        // Convert the Places bookmark object to a Sync bookmark and add
+        // kind-specific properties. Titles are required for bookmarks,
+        // folders, and livemarks; optional for queries, and omitted for
+        // separators.
+        let kind = await getKindForItem(db, bookmarkItem);
+        let item;
+        switch (kind) {
+          case BookmarkSyncUtils.KINDS.BOOKMARK:
+            item = await fetchBookmarkItem(db, bookmarkItem);
+            break;
 
-      case BookmarkSyncUtils.KINDS.QUERY:
-        item = await fetchQueryItem(bookmarkItem);
-        break;
-
-      case BookmarkSyncUtils.KINDS.FOLDER:
-        item = await fetchFolderItem(bookmarkItem);
-        break;
+          case BookmarkSyncUtils.KINDS.QUERY:
+            item = await fetchQueryItem(db, bookmarkItem);
+            break;
 
-      case BookmarkSyncUtils.KINDS.LIVEMARK:
-        item = await fetchLivemarkItem(bookmarkItem);
-        break;
-
-      case BookmarkSyncUtils.KINDS.SEPARATOR:
-        item = await placesBookmarkToSyncBookmark(bookmarkItem);
-        item.index = bookmarkItem.index;
-        break;
+          case BookmarkSyncUtils.KINDS.FOLDER:
+            item = await fetchFolderItem(db, bookmarkItem);
+            break;
 
-      default:
-        throw new Error(`Unknown bookmark kind: ${kind}`);
-    }
+          case BookmarkSyncUtils.KINDS.LIVEMARK:
+            item = await fetchLivemarkItem(db, bookmarkItem);
+            break;
 
-    // Sync uses the parent title for de-duping. All Sync bookmark objects
-    // except the Places root should have this property.
-    if (bookmarkItem.parentGuid) {
-      let parent = await PlacesUtils.bookmarks.fetch(bookmarkItem.parentGuid);
-      item.parentTitle = parent.title || "";
-    }
-
-    return item;
-  },
+          case BookmarkSyncUtils.KINDS.SEPARATOR:
+            item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
+            item.index = bookmarkItem.index;
+            break;
 
-  /**
-   * Get the sync record kind for the record with provided sync id.
-   *
-   * @param syncId
-   *        Sync ID for the item in question
-   *
-   * @returns {Promise} A promise that resolves with the sync record kind (e.g.
-   *                    something under `PlacesSyncUtils.bookmarks.KIND`), or
-   *                    with `null` if no item with that guid exists.
-   * @throws if `guid` is invalid.
-   */
-  getKindForSyncId(syncId) {
-    PlacesUtils.SYNC_BOOKMARK_VALIDATORS.syncId(syncId);
-    let guid = BookmarkSyncUtils.syncIdToGuid(syncId);
-    return PlacesUtils.bookmarks.fetch(guid)
-    .then(item => {
-      if (!item) {
-        return null;
+          default:
+            throw new Error(`Unknown bookmark kind: ${kind}`);
+        }
+
+        // Sync uses the parent title for de-duping. All Sync bookmark objects
+        // except the Places root should have this property.
+        if (bookmarkItem.parentGuid) {
+          let parent = await PlacesUtils.bookmarks.fetch(bookmarkItem.parentGuid);
+          item.parentTitle = parent.title || "";
+        }
+
+        return item;
       }
-      return getKindForItem(item)
-    });
+    );
   },
 
   /**
    * Returns the sync change counter increment for a change source constant.
    */
   determineSyncChangeDelta(source) {
     // Don't bump the change counter when applying changes made by Sync, to
     // avoid sync loops.
@@ -840,17 +828,26 @@ const BookmarkSyncUtils = PlacesSyncUtil
    * and `serverMillis`.
    */
   ratchetTimestampBackwards(existingMillis, serverMillis, lowerBound = BookmarkSyncUtils.EARLIEST_BOOKMARK_TIMESTAMP) {
     const possible = [+existingMillis, +serverMillis].filter(n => !isNaN(n) && n > lowerBound);
     if (!possible.length) {
       return undefined;
     }
     return Math.min(...possible);
-  }
+  },
+
+  /**
+   * Fetches an array of GUIDs for items that have an annotation set with the
+   * given value.
+   */
+  async fetchGuidsWithAnno(anno, val) {
+    let db = await PlacesUtils.promiseDBConnection();
+    return fetchGuidsWithAnno(db, anno, val);
+  },
 });
 
 XPCOMUtils.defineLazyGetter(this, "BookmarkSyncLog", () => {
   return Log.repository.getLogger("BookmarkSyncUtils");
 });
 
 function validateSyncBookmarkObject(name, input, behavior) {
   return PlacesUtils.validateItemProperties(name,
@@ -891,54 +888,55 @@ var GUIDMissing = async function(guid) {
     }
     throw ex;
   }
 };
 
 // Tag queries use a `place:` URL that refers to the tag folder ID. When we
 // apply a synced tag query from a remote client, we need to update the URL to
 // point to the local tag folder.
-var updateTagQueryFolder = async function(info) {
+async function updateTagQueryFolder(db, info) {
   if (info.kind != BookmarkSyncUtils.KINDS.QUERY || !info.folder || !info.url ||
       info.url.protocol != "place:") {
     return info;
   }
 
   let params = new URLSearchParams(info.url.pathname);
   let type = +params.get("type");
 
   if (type != Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS) {
     return info;
   }
 
-  let id = await getOrCreateTagFolder(info.folder);
+  let id = await getOrCreateTagFolder(db, info.folder);
   BookmarkSyncLog.debug(`updateTagQueryFolder: Tag query folder: ${
     info.folder} = ${id}`);
 
   // Rewrite the query to reference the new ID.
   params.set("folder", id);
   info.url = new URL(info.url.protocol + params);
 
   return info;
-};
+}
 
-var annotateOrphan = async function(item, requestedParentSyncId) {
+async function annotateOrphan(item, requestedParentSyncId) {
   let guid = BookmarkSyncUtils.syncIdToGuid(item.syncId);
   let itemId = await PlacesUtils.promiseItemId(guid);
   PlacesUtils.annotations.setItemAnnotation(itemId,
     BookmarkSyncUtils.SYNC_PARENT_ANNO, requestedParentSyncId, 0,
     PlacesUtils.annotations.EXPIRE_NEVER,
     SOURCE_SYNC);
-};
+}
 
-var reparentOrphans = async function(item) {
+var reparentOrphans = async function(db, item) {
   if (!item.kind || item.kind != BookmarkSyncUtils.KINDS.FOLDER) {
     return;
   }
-  let orphanGuids = await fetchGuidsWithAnno(BookmarkSyncUtils.SYNC_PARENT_ANNO,
+  let orphanGuids = await fetchGuidsWithAnno(db,
+                                             BookmarkSyncUtils.SYNC_PARENT_ANNO,
                                              item.syncId);
   let folderGuid = BookmarkSyncUtils.syncIdToGuid(item.syncId);
   BookmarkSyncLog.debug(`reparentOrphans: Reparenting ${
     JSON.stringify(orphanGuids)} to ${item.syncId}`);
   for (let i = 0; i < orphanGuids.length; ++i) {
     try {
       // Reparenting can fail if we have a corrupted or incomplete tree
       // where an item's parent is one of its descendants.
@@ -950,20 +948,20 @@ var reparentOrphans = async function(ite
         index: PlacesUtils.bookmarks.DEFAULT_INDEX,
         source: SOURCE_SYNC,
       });
     } catch (ex) {
       BookmarkSyncLog.error(`reparentOrphans: Failed to reparent item ${
         orphanGuids[i]} to ${item.syncId}`, ex);
     }
   }
-};
+}
 
 // Inserts a synced bookmark into the database.
-var insertSyncBookmark = async function(insertInfo) {
+async function insertSyncBookmark(db, insertInfo) {
   let requestedParentSyncId = insertInfo.parentSyncId;
   let requestedParentGuid =
     BookmarkSyncUtils.syncIdToGuid(insertInfo.parentSyncId);
   let isOrphan = await GUIDMissing(requestedParentGuid);
 
   // Default to "unfiled" for new bookmarks if the parent doesn't exist.
   if (!isOrphan) {
     BookmarkSyncLog.debug(`insertSyncBookmark: Item ${
@@ -972,64 +970,64 @@ var insertSyncBookmark = async function(
     BookmarkSyncLog.debug(`insertSyncBookmark: Item ${
       insertInfo.syncId} is an orphan: parent ${
       insertInfo.parentSyncId} doesn't exist; reparenting to unfiled`);
     insertInfo.parentSyncId = "unfiled";
   }
 
   // If we're inserting a tag query, make sure the tag exists and fix the
   // folder ID to refer to the local tag folder.
-  insertInfo = await updateTagQueryFolder(insertInfo);
+  insertInfo = await updateTagQueryFolder(db, insertInfo);
 
   let newItem;
   if (insertInfo.kind == BookmarkSyncUtils.KINDS.LIVEMARK) {
-    newItem = await insertSyncLivemark(insertInfo);
+    newItem = await insertSyncLivemark(db, insertInfo);
   } else {
     let bookmarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
     let bookmarkItem = await PlacesUtils.bookmarks.insert(bookmarkInfo);
-    newItem = await insertBookmarkMetadata(bookmarkItem, insertInfo);
+    newItem = await insertBookmarkMetadata(db, bookmarkItem, insertInfo);
   }
 
   if (!newItem) {
     return null;
   }
 
   // If the item is an orphan, annotate it with its real parent sync ID.
   if (isOrphan) {
     await annotateOrphan(newItem, requestedParentSyncId);
   }
 
   // Reparent all orphans that expect this folder as the parent.
-  await reparentOrphans(newItem);
+  await reparentOrphans(db, newItem);
 
   return newItem;
-};
+}
 
 // Inserts a synced livemark.
-var insertSyncLivemark = async function(insertInfo) {
+async function insertSyncLivemark(db, insertInfo) {
   if (!insertInfo.feed) {
     BookmarkSyncLog.debug(`insertSyncLivemark: ${
       insertInfo.syncId} missing feed URL`);
     return null;
   }
   let livemarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
-  let parentIsLivemark = await getAnno(livemarkInfo.parentGuid,
+  let parentIsLivemark = await getAnno(db, livemarkInfo.parentGuid,
                                        PlacesUtils.LMANNO_FEEDURI);
   if (parentIsLivemark) {
     // A livemark can't be a descendant of another livemark.
     BookmarkSyncLog.debug(`insertSyncLivemark: Invalid parent ${
       insertInfo.parentSyncId}; skipping livemark record ${
       insertInfo.syncId}`);
     return null;
   }
 
   let livemarkItem = await PlacesUtils.livemarks.addLivemark(livemarkInfo);
 
-  return insertBookmarkMetadata(livemarkItem, insertInfo);
-};
+  return insertBookmarkMetadata(db, livemarkItem, insertInfo);
+}
 
 // Keywords are a 1 to 1 mapping between strings and pairs of (URL, postData).
 // (the postData is not synced, so we ignore it). Sync associates keywords with
 // bookmarks, which is not really accurate. -- We might already have a keyword
 // with that name, or we might already have another bookmark with that URL with
 // a different keyword, etc.
 //
 // If we don't handle those cases by removing the conflicting keywords first,
@@ -1066,19 +1064,19 @@ function removeConflictingKeywords(bookm
           db, entryForNewKeyword.url, 1);
       }
     }
   );
 }
 
 // Sets annotations, keywords, and tags on a new bookmark. Returns a Sync
 // bookmark object.
-var insertBookmarkMetadata = async function(bookmarkItem, insertInfo) {
+async function insertBookmarkMetadata(db, bookmarkItem, insertInfo) {
   let itemId = await PlacesUtils.promiseItemId(bookmarkItem.guid);
-  let newItem = await placesBookmarkToSyncBookmark(bookmarkItem);
+  let newItem = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
   if (insertInfo.query) {
     PlacesUtils.annotations.setItemAnnotation(itemId,
       BookmarkSyncUtils.SMART_BOOKMARKS_ANNO, insertInfo.query, 0,
       PlacesUtils.annotations.EXPIRE_NEVER,
       SOURCE_SYNC);
     newItem.query = insertInfo.query;
   }
@@ -1112,37 +1110,37 @@ var insertBookmarkMetadata = async funct
     PlacesUtils.annotations.setItemAnnotation(itemId,
       BookmarkSyncUtils.SIDEBAR_ANNO, insertInfo.loadInSidebar, 0,
       PlacesUtils.annotations.EXPIRE_NEVER,
       SOURCE_SYNC);
     newItem.loadInSidebar = insertInfo.loadInSidebar;
   }
 
   return newItem;
-};
+}
 
 // Determines the Sync record kind for an existing bookmark.
-var getKindForItem = async function(item) {
+async function getKindForItem(db, item) {
   switch (item.type) {
     case PlacesUtils.bookmarks.TYPE_FOLDER: {
-      let isLivemark = await getAnno(item.guid,
+      let isLivemark = await getAnno(db, item.guid,
                                      PlacesUtils.LMANNO_FEEDURI);
       return isLivemark ? BookmarkSyncUtils.KINDS.LIVEMARK :
                           BookmarkSyncUtils.KINDS.FOLDER;
     }
     case PlacesUtils.bookmarks.TYPE_BOOKMARK:
       return item.url.protocol == "place:" ?
              BookmarkSyncUtils.KINDS.QUERY :
              BookmarkSyncUtils.KINDS.BOOKMARK;
 
     case PlacesUtils.bookmarks.TYPE_SEPARATOR:
       return BookmarkSyncUtils.KINDS.SEPARATOR;
   }
   return null;
-};
+}
 
 // Returns the `nsINavBookmarksService` bookmark type constant for a Sync
 // record kind.
 function getTypeForKind(kind) {
   switch (kind) {
     case BookmarkSyncUtils.KINDS.BOOKMARK:
     case BookmarkSyncUtils.KINDS.QUERY:
       return PlacesUtils.bookmarks.TYPE_BOOKMARK;
@@ -1182,17 +1180,17 @@ var shouldReinsertLivemark = async funct
     let siteURI = PlacesUtils.toURI(updateInfo.site);
     if (!livemark.siteURI || !siteURI.equals(livemark.siteURI)) {
       return true;
     }
   }
   return false;
 };
 
-var updateSyncBookmark = async function(updateInfo) {
+async function updateSyncBookmark(db, updateInfo) {
   let guid = BookmarkSyncUtils.syncIdToGuid(updateInfo.syncId);
   let oldBookmarkItem = await PlacesUtils.bookmarks.fetch(guid);
   if (!oldBookmarkItem) {
     throw new Error(`Bookmark with sync ID ${
       updateInfo.syncId} does not exist`);
   }
 
   if (updateInfo.hasOwnProperty("dateAdded")) {
@@ -1201,17 +1199,17 @@ var updateSyncBookmark = async function(
     if (!newDateAdded || newDateAdded === oldBookmarkItem.dateAdded) {
       delete updateInfo.dateAdded;
     } else {
       updateInfo.dateAdded = newDateAdded;
     }
   }
 
   let shouldReinsert = false;
-  let oldKind = await getKindForItem(oldBookmarkItem);
+  let oldKind = await getKindForItem(db, oldBookmarkItem);
   if (updateInfo.hasOwnProperty("kind") && updateInfo.kind != oldKind) {
     // If the item's aren't the same kind, we can't update the record;
     // we must remove and reinsert.
     shouldReinsert = true;
     if (BookmarkSyncLog.level <= Log.Level.Warn) {
       let oldSyncId = BookmarkSyncUtils.guidToSyncId(oldBookmarkItem.guid);
       BookmarkSyncLog.warn(`updateSyncBookmark: Local ${
         oldSyncId} kind = ${oldKind}; remote ${
@@ -1241,17 +1239,17 @@ var updateSyncBookmark = async function(
       source: SOURCE_SYNC,
     });
     // A reinsertion likely indicates a confused client, since there aren't
     // public APIs for changing livemark URLs or an item's kind (e.g., turning
     // a folder into a separator while preserving its annos and position).
     // This might be a good case to repair later; for now, we assume Sync has
     // passed a complete record for the new item, and don't try to merge
     // `oldBookmarkItem` with `updateInfo`.
-    return insertSyncBookmark(newInfo);
+    return insertSyncBookmark(db, newInfo);
   }
 
   let isOrphan = false, requestedParentSyncId;
   if (updateInfo.hasOwnProperty("parentSyncId")) {
     requestedParentSyncId = updateInfo.parentSyncId;
     let oldParentSyncId =
       BookmarkSyncUtils.guidToSyncId(oldBookmarkItem.parentGuid);
     if (requestedParentSyncId != oldParentSyncId) {
@@ -1276,43 +1274,43 @@ var updateSyncBookmark = async function(
       }
     } else {
       // If the parent is the same, just omit it so that `update` doesn't do
       // extra work.
       delete updateInfo.parentSyncId;
     }
   }
 
-  updateInfo = await updateTagQueryFolder(updateInfo);
+  updateInfo = await updateTagQueryFolder(db, updateInfo);
 
   let bookmarkInfo = syncBookmarkToPlacesBookmark(updateInfo);
   let newBookmarkItem = shouldUpdateBookmark(bookmarkInfo) ?
                         await PlacesUtils.bookmarks.update(bookmarkInfo) :
                         oldBookmarkItem;
-  let newItem = await updateBookmarkMetadata(oldBookmarkItem, newBookmarkItem,
-                                             updateInfo);
+  let newItem = await updateBookmarkMetadata(db, oldBookmarkItem,
+                                             newBookmarkItem, updateInfo);
 
   // If the item is an orphan, annotate it with its real parent sync ID.
   if (isOrphan) {
     await annotateOrphan(newItem, requestedParentSyncId);
   }
 
   // Reparent all orphans that expect this folder as the parent.
-  await reparentOrphans(newItem);
+  await reparentOrphans(db, newItem);
 
   return newItem;
-};
+}
 
 // Updates tags, keywords, and annotations for an existing bookmark. Returns a
 // Sync bookmark object.
-var updateBookmarkMetadata = async function(oldBookmarkItem,
-                                                   newBookmarkItem,
-                                                   updateInfo) {
+async function updateBookmarkMetadata(db, oldBookmarkItem,
+                                      newBookmarkItem,
+                                      updateInfo) {
   let itemId = await PlacesUtils.promiseItemId(newBookmarkItem.guid);
-  let newItem = await placesBookmarkToSyncBookmark(newBookmarkItem);
+  let newItem = await placesBookmarkToSyncBookmark(db, newBookmarkItem);
 
   try {
     newItem.tags = tagItem(newBookmarkItem, updateInfo.tags);
   } catch (ex) {
     BookmarkSyncLog.warn(`updateBookmarkMetadata: Error tagging item ${
       updateInfo.syncId}`, ex);
   }
 
@@ -1359,17 +1357,17 @@ var updateBookmarkMetadata = async funct
     PlacesUtils.annotations.setItemAnnotation(itemId,
       BookmarkSyncUtils.SMART_BOOKMARKS_ANNO, updateInfo.query, 0,
       PlacesUtils.annotations.EXPIRE_NEVER,
       SOURCE_SYNC);
     newItem.query = updateInfo.query;
   }
 
   return newItem;
-};
+}
 
 function validateNewBookmark(name, info) {
   let insertInfo = validateSyncBookmarkObject(name, info,
     { kind: { required: true },
       syncId: { required: true },
       url: { requiredIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
                                 BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind),
             validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK,
@@ -1395,41 +1393,38 @@ function validateNewBookmark(name, info)
       feed: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK },
       site: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK },
       dateAdded: { required: false }
     });
 
   return insertInfo;
 }
 
-// Returns an array of GUIDs for items that have an `anno` with the given `val`.
-var fetchGuidsWithAnno = async function(anno, val) {
-  let db = await PlacesUtils.promiseDBConnection();
+async function fetchGuidsWithAnno(db, anno, val) {
   let rows = await db.executeCached(`
     SELECT b.guid FROM moz_items_annos a
     JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
     JOIN moz_bookmarks b ON b.id = a.item_id
     WHERE n.name = :anno AND
           a.content = :val`,
     { anno, val });
   return rows.map(row => row.getResultByName("guid"));
-};
+}
 
 // Returns the value of an item's annotation, or `null` if it's not set.
-var getAnno = async function(guid, anno) {
-  let db = await PlacesUtils.promiseDBConnection();
+async function getAnno(db, guid, anno) {
   let rows = await db.executeCached(`
     SELECT a.content FROM moz_items_annos a
     JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
     JOIN moz_bookmarks b ON b.id = a.item_id
     WHERE b.guid = :guid AND
           n.name = :anno`,
     { guid, anno });
   return rows.length ? rows[0].getResultByName("content") : null;
-};
+}
 
 function tagItem(item, tags) {
   if (!item.url) {
     return [];
   }
 
   // Remove leading and trailing whitespace, then filter out empty tags.
   let newTags = tags ? tags.map(tag => tag.trim()).filter(Boolean) : [];
@@ -1453,49 +1448,48 @@ function tagItem(item, tags) {
 // having it throw in case we only pass properties like `{ guid, feedURI }`.
 function shouldUpdateBookmark(bookmarkInfo) {
   return bookmarkInfo.hasOwnProperty("parentGuid") ||
          bookmarkInfo.hasOwnProperty("title") ||
          bookmarkInfo.hasOwnProperty("url");
 }
 
 // Returns the folder ID for `tag`, or `null` if the tag doesn't exist.
-var getTagFolder = async function(tag) {
-  let db = await PlacesUtils.promiseDBConnection();
+async function getTagFolder(db, tag) {
   let results = await db.executeCached(`
     SELECT id
     FROM moz_bookmarks
     WHERE type = :type AND
           parent = :tagsFolderId AND
           title = :tag`,
     { type: PlacesUtils.bookmarks.TYPE_FOLDER,
       tagsFolderId: PlacesUtils.tagsFolderId, tag });
   return results.length ? results[0].getResultByName("id") : null;
-};
+}
 
 // Returns the folder ID for `tag`, creating one if it doesn't exist.
-var getOrCreateTagFolder = async function(tag) {
-  let id = await getTagFolder(tag);
+async function getOrCreateTagFolder(db, tag) {
+  let id = await getTagFolder(db, tag);
   if (id) {
     return id;
   }
   // Create the tag if it doesn't exist.
   let item = await PlacesUtils.bookmarks.insert({
     type: PlacesUtils.bookmarks.TYPE_FOLDER,
     parentGuid: PlacesUtils.bookmarks.tagsGuid,
     title: tag,
     source: SOURCE_SYNC,
   });
   return PlacesUtils.promiseItemId(item.guid);
-};
+}
 
 // Converts a Places bookmark or livemark to a Sync bookmark. This function
 // maps Places GUIDs to sync IDs and filters out extra Places properties like
 // date added, last modified, and index.
-var placesBookmarkToSyncBookmark = async function(bookmarkItem) {
+async function placesBookmarkToSyncBookmark(db, bookmarkItem) {
   let item = {};
 
   for (let prop in bookmarkItem) {
     switch (prop) {
       // Sync IDs are identical to Places GUIDs for all items except roots.
       case "guid":
         item.syncId = BookmarkSyncUtils.guidToSyncId(bookmarkItem.guid);
         break;
@@ -1503,17 +1497,17 @@ var placesBookmarkToSyncBookmark = async
       case "parentGuid":
         item.parentSyncId =
           BookmarkSyncUtils.guidToSyncId(bookmarkItem.parentGuid);
         break;
 
       // Sync uses kinds instead of types, which distinguish between folders,
       // livemarks, bookmarks, and queries.
       case "type":
-        item.kind = await getKindForItem(bookmarkItem);
+        item.kind = await getKindForItem(db, bookmarkItem);
         break;
 
       case "title":
       case "url":
         item[prop] = bookmarkItem[prop];
         break;
 
       case "dateAdded":
@@ -1530,17 +1524,17 @@ var placesBookmarkToSyncBookmark = async
         if (bookmarkItem.siteURI) {
           item.site = new URL(bookmarkItem.siteURI.spec);
         }
         break;
     }
   }
 
   return item;
-};
+}
 
 // Converts a Sync bookmark object to a Places bookmark or livemark object.
 // This function maps sync IDs to Places GUIDs, and filters out extra Sync
 // properties like keywords, tags, and descriptions. Returns an object that can
 // be passed to `PlacesUtils.livemarks.addLivemark` or
 // `PlacesUtils.bookmarks.{insert, update}`.
 function syncBookmarkToPlacesBookmark(info) {
   let bookmarkInfo = {
@@ -1590,101 +1584,102 @@ function syncBookmarkToPlacesBookmark(in
     }
   }
 
   return bookmarkInfo;
 }
 
 // Creates and returns a Sync bookmark object containing the bookmark's
 // tags, keyword, description, and whether it loads in the sidebar.
-var fetchBookmarkItem = async function(bookmarkItem) {
-  let item = await placesBookmarkToSyncBookmark(bookmarkItem);
+var fetchBookmarkItem = async function(db, bookmarkItem) {
+  let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
   if (!item.title) {
     item.title = "";
   }
 
   item.tags = PlacesUtils.tagging.getTagsForURI(
     PlacesUtils.toURI(bookmarkItem.url), {});
 
   let keywordEntry = await PlacesUtils.keywords.fetch({
     url: bookmarkItem.url,
   });
   if (keywordEntry) {
     item.keyword = keywordEntry.keyword;
   }
 
-  let description = await getAnno(bookmarkItem.guid,
+  let description = await getAnno(db, bookmarkItem.guid,
                                   BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
-  item.loadInSidebar = !!(await getAnno(bookmarkItem.guid,
+  item.loadInSidebar = !!(await getAnno(db, bookmarkItem.guid,
                                         BookmarkSyncUtils.SIDEBAR_ANNO));
 
   return item;
 };
 
 // Creates and returns a Sync bookmark object containing the folder's
 // description and children.
-var fetchFolderItem = async function(bookmarkItem) {
-  let item = await placesBookmarkToSyncBookmark(bookmarkItem);
+async function fetchFolderItem(db, bookmarkItem) {
+  let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
   if (!item.title) {
     item.title = "";
   }
 
-  let description = await getAnno(bookmarkItem.guid,
+  let description = await getAnno(db, bookmarkItem.guid,
                                   BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
-  let db = await PlacesUtils.promiseDBConnection();
   let childGuids = await fetchChildGuids(db, bookmarkItem.guid);
   item.childSyncIds = childGuids.map(guid =>
     BookmarkSyncUtils.guidToSyncId(guid)
   );
 
   return item;
-};
+}
 
 // Creates and returns a Sync bookmark object containing the livemark's
 // description, children (none), feed URI, and site URI.
-var fetchLivemarkItem = async function(bookmarkItem) {
-  let item = await placesBookmarkToSyncBookmark(bookmarkItem);
+async function fetchLivemarkItem(db, bookmarkItem) {
+  let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
   if (!item.title) {
     item.title = "";
   }
 
-  let description = await getAnno(bookmarkItem.guid,
+  let description = await getAnno(db, bookmarkItem.guid,
                                   BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
-  let feedAnno = await getAnno(bookmarkItem.guid, PlacesUtils.LMANNO_FEEDURI);
+  let feedAnno = await getAnno(db, bookmarkItem.guid,
+                               PlacesUtils.LMANNO_FEEDURI);
   item.feed = new URL(feedAnno);
 
-  let siteAnno = await getAnno(bookmarkItem.guid, PlacesUtils.LMANNO_SITEURI);
+  let siteAnno = await getAnno(db, bookmarkItem.guid,
+                               PlacesUtils.LMANNO_SITEURI);
   if (siteAnno) {
     item.site = new URL(siteAnno);
   }
 
   return item;
-};
+}
 
 // Creates and returns a Sync bookmark object containing the query's tag
 // folder name and smart bookmark query ID.
-var fetchQueryItem = async function(bookmarkItem) {
-  let item = await placesBookmarkToSyncBookmark(bookmarkItem);
+async function fetchQueryItem(db, bookmarkItem) {
+  let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
-  let description = await getAnno(bookmarkItem.guid,
+  let description = await getAnno(db, bookmarkItem.guid,
                                   BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
   let folder = null;
   let params = new URLSearchParams(bookmarkItem.url.pathname);
   let tagFolderId = +params.get("folder");
@@ -1697,24 +1692,24 @@ var fetchQueryItem = async function(book
       BookmarkSyncLog.warn("fetchQueryItem: Query " + bookmarkItem.url.href +
                            " points to nonexistent folder " + tagFolderId, ex);
     }
   }
   if (folder != null) {
     item.folder = folder;
   }
 
-  let query = await getAnno(bookmarkItem.guid,
+  let query = await getAnno(db, bookmarkItem.guid,
                             BookmarkSyncUtils.SMART_BOOKMARKS_ANNO);
   if (query) {
     item.query = query;
   }
 
   return item;
-};
+}
 
 function addRowToChangeRecords(row, changeRecords) {
   let syncId = BookmarkSyncUtils.guidToSyncId(row.getResultByName("guid"));
   let modifiedAsPRTime = row.getResultByName("modified");
   let modified = modifiedAsPRTime / MICROSECONDS_PER_SECOND;
   if (Number.isNaN(modified) || modified <= 0) {
     BookmarkSyncLog.error("addRowToChangeRecords: Invalid modified date for " +
                           syncId, modifiedAsPRTime);
@@ -1884,22 +1879,21 @@ var dedupeSyncBookmark = async function(
     }
   }
 
   return changeRecords;
 };
 
 // Moves a synced folder's remaining children to its parent, and deletes the
 // folder if it's empty.
-var deleteSyncedFolder = async function(bookmarkItem) {
+async function deleteSyncedFolder(db, bookmarkItem) {
   // At this point, any member in the folder that remains is either a folder
   // pending deletion (which we'll get to in this function), or an item that
   // should not be deleted. To avoid deleting these items, we first move them
   // to the parent of the folder we're about to delete.
-  let db = await PlacesUtils.promiseDBConnection();
   let childGuids = await fetchChildGuids(db, bookmarkItem.guid);
   if (!childGuids.length) {
     // No children -- just delete the folder.
     return deleteSyncedAtom(bookmarkItem);
   }
 
   if (BookmarkSyncLog.level <= Log.Level.Trace) {
     BookmarkSyncLog.trace(
@@ -1945,17 +1939,17 @@ var deleteSyncedFolder = async function(
     // (Ideally this whole operation would be done in a transaction, and this
     // wouldn't be possible).
     BookmarkSyncLog.trace(`deleteSyncedFolder: Error removing parent ` +
                           `${bookmarkItem.guid} after reparenting children`, e);
     return false;
   }
 
   return true;
-};
+}
 
 // Removes a synced bookmark or empty folder from the database.
 var deleteSyncedAtom = async function(bookmarkItem) {
   try {
     await PlacesUtils.bookmarks.remove(bookmarkItem.guid, {
       preventRemovalOfNonEmptyFolders: true,
       source: SOURCE_SYNC,
     });
--- a/toolkit/components/places/tests/unit/test_sync_utils.js
+++ b/toolkit/components/places/tests/unit/test_sync_utils.js
@@ -1,15 +1,10 @@
 Cu.import("resource://gre/modules/ObjectUtils.jsm");
 Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
-const {
-  // `fetchGuidsWithAnno` isn't exported, but we can still access it here via a
-  // backstage pass.
-  fetchGuidsWithAnno,
-} = Cu.import("resource://gre/modules/PlacesSyncUtils.jsm", {});
 Cu.import("resource://testing-common/httpd.js");
 Cu.importGlobalProperties(["URLSearchParams"]);
 
 const DESCRIPTION_ANNO = "bookmarkProperties/description";
 const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
 const SYNC_PARENT_ANNO = "sync/parent";
 
 var makeGuid = PlacesUtils.history.makeGuid;
@@ -1508,42 +1503,42 @@ add_task(async function test_move_orphan
     kind: "bookmark",
     syncId: makeGuid(),
     parentSyncId: nonexistentSyncId,
     url: "http://getthunderbird.com",
   });
 
   do_print("Verify synced orphan annos match");
   {
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids.sort(), [fxBmk.syncId, tbBmk.syncId].sort(),
       "Orphaned bookmarks should match before moving");
   }
 
   do_print("Move synced orphan using async API");
   {
     await PlacesUtils.bookmarks.update({
       guid: fxBmk.syncId,
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       index: PlacesUtils.bookmarks.DEFAULT_INDEX,
     });
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids, [tbBmk.syncId],
       "Should remove orphan annos from updated bookmark");
   }
 
   do_print("Move synced orphan using sync API");
   {
     let tbId = await syncIdToId(tbBmk.syncId);
     PlacesUtils.bookmarks.moveItem(tbId, PlacesUtils.toolbarFolderId,
       PlacesUtils.bookmarks.DEFAULT_INDEX);
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids, [],
       "Should remove orphan annos from moved bookmark");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
@@ -1565,31 +1560,31 @@ add_task(async function test_reorder_orp
     kind: "bookmark",
     syncId: makeGuid(),
     parentSyncId: nonexistentSyncId,
     url: "https://mozilla.org",
   });
 
   do_print("Verify synced orphan annos match");
   {
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids.sort(), [
       fxBmk.syncId,
       tbBmk.syncId,
       mozBmk.syncId,
     ].sort(), "Orphaned bookmarks should match before reordering");
   }
 
   do_print("Reorder synced orphans");
   {
     await PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid,
       [tbBmk.syncId, fxBmk.syncId]);
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids, [mozBmk.syncId],
       "Should remove orphan annos from explicitly reordered bookmarks");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
@@ -1605,33 +1600,33 @@ add_task(async function test_set_orphan_
     kind: "bookmark",
     syncId: makeGuid(),
     parentSyncId: nonexistentSyncId,
     url: "http://getthunderbird.com",
   });
 
   do_print("Verify synced orphan annos match");
   {
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids.sort(), [fxBmk.syncId, tbBmk.syncId].sort(),
       "Orphaned bookmarks should match before changing indices");
   }
 
   do_print("Set synced orphan indices");
   {
     let fxId = await syncIdToId(fxBmk.syncId);
     let tbId = await syncIdToId(tbBmk.syncId);
     PlacesUtils.bookmarks.runInBatchMode(_ => {
       PlacesUtils.bookmarks.setItemIndex(fxId, 1);
       PlacesUtils.bookmarks.setItemIndex(tbId, 0);
     }, null);
     await PlacesTestUtils.promiseAsyncUpdates();
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids, [],
       "Should remove orphan annos after updating indices");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
@@ -1657,28 +1652,28 @@ add_task(async function test_unsynced_or
     syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN,
   });
 
   do_print("Move unsynced orphan");
   {
     let unknownId = await syncIdToId(unknownBmk.syncId);
     PlacesUtils.bookmarks.moveItem(unknownId, PlacesUtils.toolbarFolderId,
       PlacesUtils.bookmarks.DEFAULT_INDEX);
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids.sort(), [newBmk.syncId].sort(),
       "Should remove orphan annos from moved unsynced bookmark");
   }
 
   do_print("Reorder unsynced orphans");
   {
     await PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.unfiledGuid,
       [newBmk.syncId]);
-    let orphanGuids = await fetchGuidsWithAnno(SYNC_PARENT_ANNO,
-      nonexistentSyncId);
+    let orphanGuids = await PlacesSyncUtils.bookmarks.fetchGuidsWithAnno(
+      SYNC_PARENT_ANNO, nonexistentSyncId);
     deepEqual(orphanGuids, [],
       "Should remove orphan annos from reordered unsynced bookmarks");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });