Bug 1312226 - Use async queries to read annos in `PlacesSyncUtils`. r?markh draft
authorKit Cambridge <kit@yakshaving.ninja>
Fri, 21 Oct 2016 16:59:28 -0700
changeset 428306 ba778e571f3b8c1349058dbb3ea2c3d21384b741
parent 426145 c22f0df11dd8dc7671017fb517f5a9deb3c6fce8
child 534711 ff4918d7728702009f3630785a8e584037206516
push id33285
push userbmo:kcambridge@mozilla.com
push dateSat, 22 Oct 2016 20:44:32 +0000
reviewersmarkh
bugs1312226
milestone52.0a1
Bug 1312226 - Use async queries to read annos in `PlacesSyncUtils`. r?markh The annotations service doesn't have an async API, and reimplementing inserts and deletes would require us to fire observers properly. We can avoid blocking the main thread for reads, though. MozReview-Commit-ID: 9DH7eKZHlyn
toolkit/components/places/PlacesSyncUtils.jsm
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -447,22 +447,18 @@ var annotateOrphan = Task.async(function
     PlacesUtils.annotations.EXPIRE_NEVER,
     SOURCE_SYNC);
 });
 
 var reparentOrphans = Task.async(function* (item) {
   if (item.kind != BookmarkSyncUtils.KINDS.FOLDER) {
     return;
   }
-  let orphanIds = findAnnoItems(BookmarkSyncUtils.SYNC_PARENT_ANNO, item.syncId);
-  // The annotations API returns item IDs, but the asynchronous bookmarks
-  // API uses GUIDs. We can remove the `promiseItemGuid` calls and parallel
-  // arrays once we implement a GUID-aware annotations API.
-  let orphanGuids = yield Promise.all(orphanIds.map(id =>
-    PlacesUtils.promiseItemGuid(id)));
+  let orphanGuids = yield fetchGuidsWithAnno(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) {
     let isReparented = false;
     try {
       // Reparenting can fail if we have a corrupted or incomplete tree
       // where an item's parent is one of its descendants.
@@ -476,17 +472,18 @@ var reparentOrphans = Task.async(functio
       });
       isReparented = true;
     } catch (ex) {
       BookmarkSyncLog.error(`reparentOrphans: Failed to reparent item ${
         orphanGuids[i]} to ${item.syncId}`, ex);
     }
     if (isReparented) {
       // Remove the annotation once we've reparented the item.
-      PlacesUtils.annotations.removeItemAnnotation(orphanIds[i],
+      let orphanId = yield PlacesUtils.promiseItemId(orphanGuids[i]);
+      PlacesUtils.annotations.removeItemAnnotation(orphanId,
         BookmarkSyncUtils.SYNC_PARENT_ANNO, SOURCE_SYNC);
     }
   }
 });
 
 // Inserts a synced bookmark into the database.
 var insertSyncBookmark = Task.async(function* (insertInfo) {
   let requestedParentSyncId = insertInfo.parentSyncId;
@@ -536,19 +533,18 @@ var insertSyncBookmark = Task.async(func
 // Inserts a synced livemark.
 var insertSyncLivemark = Task.async(function* (insertInfo) {
   if (!insertInfo.feed) {
     BookmarkSyncLog.debug(`insertSyncLivemark: ${
       insertInfo.syncId} missing feed URL`);
     return null;
   }
   let livemarkInfo = syncBookmarkToPlacesBookmark(insertInfo);
-  let parentId = yield PlacesUtils.promiseItemId(livemarkInfo.parentGuid);
-  let parentIsLivemark = PlacesUtils.annotations.itemHasAnnotation(parentId,
-    PlacesUtils.LMANNO_FEEDURI);
+  let parentIsLivemark = yield getAnno(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;
   }
 
@@ -605,19 +601,18 @@ var insertBookmarkMetadata = Task.async(
 
   return newItem;
 });
 
 // Determines the Sync record kind for an existing bookmark.
 var getKindForItem = Task.async(function* (item) {
   switch (item.type) {
     case PlacesUtils.bookmarks.TYPE_FOLDER: {
-      let itemId = yield PlacesUtils.promiseItemId(item.guid);
-      let isLivemark = PlacesUtils.annotations.itemHasAnnotation(itemId,
-        PlacesUtils.LMANNO_FEEDURI);
+      let isLivemark = yield getAnno(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;
 
@@ -844,23 +839,16 @@ var updateBookmarkMetadata = Task.async(
       PlacesUtils.annotations.EXPIRE_NEVER,
       SOURCE_SYNC);
     newItem.query = updateInfo.query;
   }
 
   return newItem;
 });
 
-var setGuid = Task.async(function* (db, itemId, newGuid) {
-  yield db.executeCached(`UPDATE moz_bookmarks SET guid = :newGuid
-    WHERE id = :itemId`, { newGuid, itemId });
-  PlacesUtils.invalidateCachedGuidFor(itemId);
-  return newGuid;
-});
-
 function validateNewBookmark(info) {
   let insertInfo = validateSyncBookmarkObject(info,
     { kind: { required: true }
     , syncId: { required: true }
     , url: { requiredIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
                               , BookmarkSyncUtils.KINDS.MICROSUMMARY
                               , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind)
            , validIf: b => [ BookmarkSyncUtils.KINDS.BOOKMARK
@@ -890,21 +878,41 @@ function validateNewBookmark(info) {
                                      , BookmarkSyncUtils.KINDS.QUERY ].includes(b.kind) }
     , feed: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK }
     , site: { validIf: b => b.kind == BookmarkSyncUtils.KINDS.LIVEMARK }
     });
 
   return insertInfo;
 }
 
-function findAnnoItems(anno, val) {
-  let annos = PlacesUtils.annotations;
-  return annos.getItemsWithAnnotation(anno, {}).filter(id =>
-    annos.getItemAnnotation(id, anno) == val);
-}
+// Returns an array of GUIDs for items that have an `anno` with the given `val`.
+var fetchGuidsWithAnno = Task.async(function* (anno, val) {
+  let db = yield PlacesUtils.promiseDBConnection();
+  let rows = yield 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 = Task.async(function* (guid, anno) {
+  let db = yield PlacesUtils.promiseDBConnection();
+  let rows = yield 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;
+});
 
 var tagItem = Task.async(function (item, tags) {
   if (!item.url) {
     return [];
   }
 
   // Remove leading and trailing whitespace, then filter out empty tags.
   let newTags = tags.map(tag => tag.trim()).filter(Boolean);
@@ -1048,94 +1056,89 @@ 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 = Task.async(function* (bookmarkItem) {
-  let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
   let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
 
   item.tags = PlacesUtils.tagging.getTagsForURI(
     PlacesUtils.toURI(bookmarkItem.url), {});
 
   let keywordEntry = yield PlacesUtils.keywords.fetch({
     url: bookmarkItem.url,
   });
   if (keywordEntry) {
     item.keyword = keywordEntry.keyword;
   }
 
-  let description = getItemDescription(itemId);
+  let description = yield getAnno(bookmarkItem.guid,
+                                  BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
-  item.loadInSidebar = PlacesUtils.annotations.itemHasAnnotation(itemId,
-    BookmarkSyncUtils.SIDEBAR_ANNO);
+  item.loadInSidebar = !!(yield getAnno(bookmarkItem.guid,
+                                        BookmarkSyncUtils.SIDEBAR_ANNO));
 
   return item;
 });
 
 // Creates and returns a Sync bookmark object containing the folder's
 // description and children.
 var fetchFolderItem = Task.async(function* (bookmarkItem) {
-  let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
   let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
 
-  let description = getItemDescription(itemId);
+  let description = yield getAnno(bookmarkItem.guid,
+                                  BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
   let db = yield PlacesUtils.promiseDBConnection();
   let children = yield fetchAllChildren(db, bookmarkItem.guid);
   item.childSyncIds = children.map(child =>
     BookmarkSyncUtils.guidToSyncId(child.guid)
   );
 
   return item;
 });
 
 // Creates and returns a Sync bookmark object containing the livemark's
 // description, children (none), feed URI, and site URI.
 var fetchLivemarkItem = Task.async(function* (bookmarkItem) {
-  let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
   let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
 
-  let description = getItemDescription(itemId);
+  let description = yield getAnno(bookmarkItem.guid,
+                                  BookmarkSyncUtils.DESCRIPTION_ANNO);
   if (description) {
     item.description = description;
   }
 
-  let feedAnno = PlacesUtils.annotations.getItemAnnotation(itemId,
-    PlacesUtils.LMANNO_FEEDURI);
+  let feedAnno = yield getAnno(bookmarkItem.guid, PlacesUtils.LMANNO_FEEDURI);
   item.feed = new URL(feedAnno);
 
-  let siteAnno = null;
-  try {
-    siteAnno = PlacesUtils.annotations.getItemAnnotation(itemId,
-      PlacesUtils.LMANNO_SITEURI);
-  } catch (ex) {}
-  if (siteAnno != null) {
+  let siteAnno = yield getAnno(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 = Task.async(function* (bookmarkItem) {
-  let itemId = yield PlacesUtils.promiseItemId(bookmarkItem.guid);
   let item = yield placesBookmarkToSyncBookmark(bookmarkItem);
 
-  let description = getItemDescription(itemId);
+  let description = yield getAnno(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");
   if (tagFolderId) {
@@ -1147,31 +1150,16 @@ var fetchQueryItem = Task.async(function
       BookmarkSyncLog.warn("fetchQueryItem: Query " + bookmarkItem.url.href +
                            " points to nonexistent folder " + tagFolderId, ex);
     }
   }
   if (folder != null) {
     item.folder = folder;
   }
 
-  let query = null;
-  try {
-    // Throws if the bookmark doesn't have the smart bookmark anno.
-    query = PlacesUtils.annotations.getItemAnnotation(itemId,
-      BookmarkSyncUtils.SMART_BOOKMARKS_ANNO);
-  } catch (ex) {}
-  if (query != null) {
+  let query = yield getAnno(bookmarkItem.guid,
+                            BookmarkSyncUtils.SMART_BOOKMARKS_ANNO);
+  if (query) {
     item.query = query;
   }
 
   return item;
 });
-
-// Returns an item's description, or `null` if one isn't set.
-function getItemDescription(id) {
-  try {
-    return PlacesUtils.annotations.getItemAnnotation(id,
-      BookmarkSyncUtils.DESCRIPTION_ANNO);
-  } catch (ex) {}
-  return null;
-}
-
-