Bug 1382363 - Change most uses of `promiseDBConnection` to `withConnectionWrapper` in `PlacesSyncUtils`. r?markh draft
authorKit Cambridge <kit@yakshaving.ninja>
Tue, 25 Jul 2017 11:53:45 -0700
changeset 615339 ca712a894bca60c551d230b214850d35a9c09b3e
parent 615176 32d9d1e81cc607320a36391845917f645f7a7f72
child 639151 8f1cd65b53b94d083db0cb81f71802e240425c2c
push id70328
push userbmo:kit@mozilla.com
push dateTue, 25 Jul 2017 21:06:04 +0000
reviewersmarkh
bugs1382363
milestone56.0a1
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();
 });