Bug 1293445 - 3 - Handle Sync tag queries rewrite. r=kitcambridge draft
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 27 Mar 2018 18:30:52 +0200
changeset 778504 a86dfc7d5fd33820af0594b748c98a6f1e3c0e76
parent 778503 9f0ced3f2345fc3d6ccc46101941e64202d1f06d
child 778505 930fdffc1b06567a43c7d96dd34172d708a55bf6
push id105499
push usermak77@bonardo.net
push dateFri, 06 Apr 2018 11:20:04 +0000
reviewerskitcambridge
bugs1293445
milestone61.0a1
Bug 1293445 - 3 - Handle Sync tag queries rewrite. r=kitcambridge MozReview-Commit-ID: DUXHCbMo5fI
services/sync/tests/unit/test_bookmark_places_query_rewriting.js
toolkit/components/places/PlacesSyncUtils.jsm
toolkit/components/places/SyncedBookmarksMirror.jsm
toolkit/components/places/tests/sync/test_bookmark_kinds.js
toolkit/components/places/tests/sync/test_sync_utils.js
--- a/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
+++ b/services/sync/tests/unit/test_bookmark_places_query_rewriting.js
@@ -16,38 +16,25 @@ function makeTagRecord(id, uri) {
   tagRecord.bmkUri = uri;
   tagRecord.title = "tagtag";
   tagRecord.folderName = "bar";
   tagRecord.parentid = PlacesUtils.bookmarks.toolbarGuid;
   return tagRecord;
 }
 
 add_task(async function run_test() {
-
   let uri = "place:folder=499&type=7&queryType=1";
   let tagRecord = makeTagRecord("abcdefabcdef", uri);
 
   _("Type: " + tagRecord.type);
   _("Folder name: " + tagRecord.folderName);
   await store.applyIncoming(tagRecord);
 
-  let tagID = -1;
-  let db = await PlacesUtils.promiseDBConnection();
-  let rows = await db.execute(`
-    SELECT id FROM moz_bookmarks
-    WHERE parent = :tagsFolderId AND
-          title = :title`,
-    { tagsFolderId: PlacesUtils.tagsFolderId,
-      title: "bar" });
-  equal(rows.length, 1);
-  tagID = rows[0].getResultByName("id");
-
-  _("Tag ID: " + tagID);
   let insertedRecord = await store.createRecord("abcdefabcdef", "bookmarks");
-  Assert.equal(insertedRecord.bmkUri, uri.replace("499", tagID));
+  Assert.equal(insertedRecord.bmkUri, "place:tag=bar");
 
   _("... but not if the type is wrong.");
   let wrongTypeURI = "place:folder=499&type=2&queryType=1";
   let wrongTypeRecord = makeTagRecord("fedcbafedcba", wrongTypeURI);
   await store.applyIncoming(wrongTypeRecord);
 
   insertedRecord = await store.createRecord("fedcbafedcba", "bookmarks");
   Assert.equal(insertedRecord.bmkUri, wrongTypeURI);
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -1397,40 +1397,39 @@ var GUIDMissing = async function(guid) {
   } catch (ex) {
     if (ex.message == "no item found for the given GUID") {
       return true;
     }
     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.
-async function updateTagQueryFolder(db, info) {
+// Legacy tag queries may 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.
+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(db, info.folder);
-  BookmarkSyncLog.debug(`updateTagQueryFolder: Tag query folder: ${
-    info.folder} = ${id}`);
+  BookmarkSyncLog.debug(`updateTagQueryFolder: Tag query folder: ${info.folder}`);
 
-  // Rewrite the query to reference the new ID.
-  params.set("folder", id);
+  // Rewrite the query to directly reference the tag.
+  params.delete("queryType");
+  params.delete("type");
+  params.delete("folder");
+  params.set("tag", info.folder);
   info.url = new URL(info.url.protocol + params);
-
   return info;
 }
 
 async function annotateOrphan(item, requestedParentRecordId) {
   let guid = BookmarkSyncUtils.recordIdToGuid(item.recordId);
   let itemId = await PlacesUtils.promiseItemId(guid);
   PlacesUtils.annotations.setItemAnnotation(itemId,
     BookmarkSyncUtils.SYNC_PARENT_ANNO, requestedParentRecordId, 0,
@@ -1959,45 +1958,16 @@ function tagItem(item, tags) {
 // but doesn't know about additional livemark properties. We check this to avoid
 // 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.
-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.
-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 record IDs and filters out extra Places properties like
 // date added, last modified, and index.
 async function placesBookmarkToSyncBookmark(db, bookmarkItem) {
   let item = {};
 
   for (let prop in bookmarkItem) {
     switch (prop) {
@@ -2187,31 +2157,20 @@ async function fetchQueryItem(db, bookma
   let item = await placesBookmarkToSyncBookmark(db, bookmarkItem);
 
   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");
-  if (tagFolderId) {
-    try {
-      let tagFolderGuid = await PlacesUtils.promiseItemGuid(tagFolderId);
-      let tagFolder = await PlacesUtils.bookmarks.fetch(tagFolderGuid);
-      folder = tagFolder.title;
-    } catch (ex) {
-      BookmarkSyncLog.warn("fetchQueryItem: Query " + bookmarkItem.url.href +
-                           " points to nonexistent folder " + tagFolderId, ex);
-    }
-  }
-  if (folder != null) {
-    item.folder = folder;
+  let tags = params.getAll("tag");
+  if (tags.length == 1) {
+    item.folder = tags[0];
   }
 
   let query = await getAnno(db, bookmarkItem.guid,
                             BookmarkSyncUtils.SMART_BOOKMARKS_ANNO);
   if (query) {
     item.query = query;
   }
 
--- a/toolkit/components/places/SyncedBookmarksMirror.jsm
+++ b/toolkit/components/places/SyncedBookmarksMirror.jsm
@@ -1351,64 +1351,46 @@ class SyncedBookmarksMirror {
           needsMerge = 0
         WHERE needsMerge AND
               guid IN (${new Array(chunk.length).fill("?").join(",")})`,
         chunk);
     }
   }
 
   /**
-   * Creates local tag folders mentioned in remotely changed tag queries, then
-   * rewrites the query URLs in the mirror to point to the new local folders.
+   * Rewrites tag query URLs in the mirror to point to the tag.
    *
-   * For example, an incoming tag query of, say, "place:type=6&folder=3" means
-   * it is a query for whatever tag is defined by the local record with id=3
+   * For example, an incoming tag query of, say, "place:type=7&folder=3" means
+   * it is a query for whatever tag was defined by the local record with id=3
    * (and the incoming record has the actual tag in the folderName field).
-   * We need to ensure that the URL is adjusted so the folder ID refers to
-   * whatever local folder ID represents that tag.
-   *
-   * This can be removed once bug 1293445 lands.
    */
   async rewriteRemoteTagQueries() {
-    // Create local tag folders that don't already exist. This fires the
-    // `tagLocalPlace` trigger.
-    await this.db.execute(`
-      INSERT INTO localTags(tag)
-      SELECT v.tagFolderName FROM items v
+    let queryRows = await this.db.execute(`
+      SELECT u.id AS urlId, u.url, v.tagFolderName
+      FROM urls u
+      JOIN items v ON v.urlId = u.id
       JOIN mergeStates r ON r.mergedGuid = v.guid
       WHERE r.valueState = :valueState AND
-            v.tagFolderName NOT NULL`,
-      { valueState: BookmarkMergeState.TYPE.REMOTE });
-
-    let queryRows = await this.db.execute(`
-      SELECT u.id AS urlId, u.url, b.id AS newTagFolderId FROM urls u
-      JOIN items v ON v.urlId = u.id
-      JOIN mergeStates r ON r.mergedGuid = v.guid
-      JOIN moz_bookmarks b ON b.title = v.tagFolderName
-      JOIN moz_bookmarks p ON p.id = b.parent
-      WHERE p.guid = :tagsGuid AND
-            r.valueState = :valueState AND
             v.kind = :queryKind AND
-            v.tagFolderName NOT NULL`,
-      { tagsGuid: PlacesUtils.bookmarks.tagsGuid,
-        valueState: BookmarkMergeState.TYPE.REMOTE,
-        queryKind: SyncedBookmarksMirror.KIND.QUERY });
+            v.tagFolderName NOT NULL AND
+            CAST(get_query_param(substr(u.url, 7), "type") AS INT) = :tagContentsType
+      `, { valueState: BookmarkMergeState.TYPE.REMOTE,
+           queryKind: SyncedBookmarksMirror.KIND.QUERY,
+           tagContentsType: Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS });
 
     let urlsParams = [];
     for (let row of queryRows) {
       let url = new URL(row.getResultByName("url"));
       let tagQueryParams = new URLSearchParams(url.pathname);
-      let type = Number(tagQueryParams.get("type"));
-      if (type != Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS) {
-        continue;
-      }
-
-      // Rewrite the query URL to point to the new folder.
-      let newTagFolderId = row.getResultByName("newTagFolderId");
-      tagQueryParams.set("folder", newTagFolderId);
+
+      // Rewrite the query to directly reference the tag.
+      tagQueryParams.delete("queryType");
+      tagQueryParams.delete("type");
+      tagQueryParams.delete("folder");
+      tagQueryParams.set("tag", row.getResultByName("tagFolderName"));
 
       let newURLHref = url.protocol + tagQueryParams;
       urlsParams.push({
         urlId: row.getResultByName("urlId"),
         url: newURLHref,
       });
     }
 
@@ -1635,20 +1617,21 @@ class SyncedBookmarksMirror {
         UNION ALL
         SELECT b.id, s.level + 1 AS level FROM moz_bookmarks b
         JOIN syncedItems s ON s.id = b.parent
       )
       INSERT INTO itemsToUpload(id, guid, syncChangeCounter, parentGuid,
                                 parentTitle, dateAdded, type, title, isQuery,
                                 url, tags, description, loadInSidebar,
                                 smartBookmarkName, keyword, feedURL, siteURL,
-                                position)
+                                position, tagFolderName)
       SELECT b.id, b.guid, b.syncChangeCounter, p.guid, p.title,
              b.dateAdded / 1000, b.type, b.title,
-             IFNULL(SUBSTR(h.url, 1, 6) = 'place:', 0), h.url,
+             IFNULL(SUBSTR(h.url, 1, 6) = 'place:', 0) AS isQuery,
+             h.url,
              (SELECT GROUP_CONCAT(t.title, ',') FROM moz_bookmarks e
               JOIN moz_bookmarks t ON t.id = e.parent
               JOIN moz_bookmarks r ON r.id = t.parent
               WHERE b.type = :bookmarkType AND
                     r.guid = :tagsGuid AND
                     e.fk = h.id),
              (SELECT a.content FROM moz_items_annos a
               JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
@@ -1669,65 +1652,35 @@ class SyncedBookmarksMirror {
               WHERE b.type = :folderType AND
                     a.item_id = b.id AND
                     n.name = :feedURLAnno),
              (SELECT a.content FROM moz_items_annos a
               JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id
               WHERE b.type = :folderType AND
                     a.item_id = b.id AND
                     n.name = :siteURLAnno),
-             b.position
+             b.position,
+             (SELECT get_query_param(substr(url, 7), 'tag')
+              WHERE substr(h.url, 1, 6) = 'place:')
       FROM moz_bookmarks b
       JOIN moz_bookmarks p ON p.id = b.parent
       JOIN syncedItems s ON s.id = b.id
       LEFT JOIN moz_places h ON h.id = b.fk
       LEFT JOIN itemsToWeaklyReupload w ON w.id = b.id
       WHERE b.syncChangeCounter >= 1 OR
             w.id NOT NULL`,
       { bookmarkType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
         tagsGuid: PlacesUtils.bookmarks.tagsGuid,
         descriptionAnno: PlacesSyncUtils.bookmarks.DESCRIPTION_ANNO,
         sidebarAnno: PlacesSyncUtils.bookmarks.SIDEBAR_ANNO,
         smartBookmarkAnno: PlacesSyncUtils.bookmarks.SMART_BOOKMARKS_ANNO,
         folderType: PlacesUtils.bookmarks.TYPE_FOLDER,
         feedURLAnno: PlacesUtils.LMANNO_FEEDURI,
         siteURLAnno: PlacesUtils.LMANNO_SITEURI });
 
-    // Record tag folder names for tag queries. Parsing query URLs one by one
-    // is inefficient, but queries aren't common today, and we can remove this
-    // logic entirely once bug 1293445 lands.
-    let queryRows = await this.db.execute(`
-      SELECT id, url FROM itemsToUpload
-      WHERE isQuery`);
-
-    let tagFolderNameParams = [];
-    for (let row of queryRows) {
-      let url = new URL(row.getResultByName("url"));
-      let tagQueryParams = new URLSearchParams(url.pathname);
-      let type = Number(tagQueryParams.get("type"));
-      if (type == Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_CONTENTS) {
-        continue;
-      }
-      let tagFolderId = Number(tagQueryParams.get("folder"));
-      tagFolderNameParams.push({
-        id: row.getResultByName("id"),
-        tagFolderId,
-        folderType: PlacesUtils.bookmarks.TYPE_FOLDER,
-      });
-    }
-
-    if (tagFolderNameParams.length) {
-      await this.db.execute(`
-        UPDATE itemsToUpload SET
-          tagFolderName = (SELECT b.title FROM moz_bookmarks b
-                           WHERE b.id = :tagFolderId AND
-                                 b.type = :folderType)
-        WHERE id = :id`, tagFolderNameParams);
-    }
-
     // Record the child GUIDs of locally changed folders, which we use to
     // populate the `children` array in the record.
     await this.db.execute(`
       INSERT INTO structureToUpload(guid, parentId, position)
       SELECT b.guid, b.parent, b.position FROM moz_bookmarks b
       JOIN itemsToUpload o ON o.id = b.parent`);
 
     // Finally, stage tombstones for deleted items. Ignore conflicts if we have
@@ -2488,17 +2441,17 @@ async function initializeTempMirrorEntit
     END`);
 
   // A view of local bookmark tags. Tags, like keywords, are associated with
   // URLs, so two bookmarks with the same URL should have the same tags. Unlike
   // keywords, one tag may be associated with many different URLs. Tags are also
   // different because they're implemented as bookmarks under the hood. Each tag
   // is stored as a folder under the tags root, and tagged URLs are stored as
   // untitled bookmarks under these folders. This complexity, along with tag
-  // query rewriting, can be removed once bug 1293445 lands.
+  // query rewriting, can be removed once bug 424160 lands.
   await db.execute(`
     CREATE TEMP VIEW localTags(tagEntryId, tagEntryGuid, tagFolderId,
                                tagFolderGuid, tagEntryPosition, tagEntryType,
                                tag, placeId) AS
     SELECT b.id, b.guid, p.id, p.guid, b.position, b.type, p.title, b.fk
     FROM moz_bookmarks b
     JOIN moz_bookmarks p ON p.id = b.parent
     JOIN moz_bookmarks r ON r.id = p.parent
--- a/toolkit/components/places/tests/sync/test_bookmark_kinds.js
+++ b/toolkit/components/places/tests/sync/test_bookmark_kinds.js
@@ -264,53 +264,59 @@ add_task(async function test_queries() {
     info("Set up places");
 
     // create a tag and grab the local folder ID.
     let tag = await PlacesUtils.bookmarks.insert({
       type: PlacesUtils.bookmarks.TYPE_FOLDER,
       parentGuid: PlacesUtils.bookmarks.tagsGuid,
       title: "a-tag",
     });
-    let tagid = await PlacesUtils.promiseItemId(tag.guid);
 
     await PlacesTestUtils.markBookmarksAsSynced();
 
     await PlacesUtils.bookmarks.insertTree({
       guid: PlacesUtils.bookmarks.menuGuid,
       children: [
         {
-          // this entry has a folder= query param for a folder that exists.
+          // this entry has a tag= query param for a tag that exists.
           guid: "queryAAAAAAA",
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           title: "TAG_QUERY query",
-          url: `place:type=6&sort=14&maxResults=10&folder=${tagid}`,
+          url: `place:tag=a-tag&&sort=14&maxResults=10`,
         },
         {
-          // this entry has a folder= query param for a folder that doesn't exist.
+          // this entry has a tag= query param for a tag that doesn't exist.
           guid: "queryBBBBBBB",
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           title: "TAG_QUERY query but invalid folder id",
-          url: `place:type=6&sort=14&maxResults=10&folder=12345`,
+          url: `place:tag=b-tag&sort=14&maxResults=10`,
         },
         {
-          // this entry has no folder= query param.
+          // this entry has no tag= query param.
           guid: "queryCCCCCCC",
           type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
           title: "TAG_QUERY without a folder at all",
-          url: "place:type=6&sort=14&maxResults=10",
+          url: "place:sort=14&maxResults=10",
         },
-
-        ],
+        {
+          // this entry has only a tag= query.
+          guid: "queryDDDDDDD",
+          type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+          title: "TAG_QUERY without a folder at all",
+          url: "place:tag=a-tag",
+        },
+      ],
     });
 
     info("Create records to upload");
     let changes = await buf.apply();
     Assert.strictEqual(changes.queryAAAAAAA.cleartext.folderName, tag.title);
-    Assert.strictEqual(changes.queryBBBBBBB.cleartext.folderName, undefined);
+    Assert.strictEqual(changes.queryBBBBBBB.cleartext.folderName, "b-tag");
     Assert.strictEqual(changes.queryCCCCCCC.cleartext.folderName, undefined);
+    Assert.strictEqual(changes.queryDDDDDDD.cleartext.folderName, tag.title);
   } finally {
     await PlacesUtils.bookmarks.eraseEverything();
     await PlacesSyncUtils.bookmarks.reset();
   }
 });
 
 // Bug 632287.
 add_task(async function test_mismatched_but_compatible_folder_types() {
--- a/toolkit/components/places/tests/sync/test_sync_utils.js
+++ b/toolkit/components/places/tests/sync/test_sync_utils.js
@@ -1518,66 +1518,66 @@ add_task(async function test_insert_anno
       "Should not set sidebar anno for new bookmark");
   }
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
 });
 
 add_task(async function test_insert_tag_query() {
-  let tagFolder = -1;
-
-  info("Insert tag query for new tag");
-  {
-    deepEqual(PlacesUtils.tagging.allTags, [], "New tag should not exist yet");
-    let query = await PlacesSyncUtils.bookmarks.insert({
-      kind: "query",
-      recordId: makeGuid(),
-      parentRecordId: "toolbar",
-      url: "place:type=7&folder=90",
-      folder: "taggy",
-      title: "Tagged stuff",
-    });
-    notEqual(query.url.href, "place:type=7&folder=90",
-      "Tag query URL for new tag should differ");
-
-    [, tagFolder] = /\bfolder=(\d+)\b/.exec(query.url.pathname);
-    ok(tagFolder > 0, "New tag query URL should contain valid folder");
-    deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "New tag should exist");
-  }
-
-  info("Insert tag query for existing tag");
-  {
-    let url = "place:type=7&folder=90&maxResults=15";
-    let query = await PlacesSyncUtils.bookmarks.insert({
-      kind: "query",
-      url,
-      folder: "taggy",
-      title: "Sorted and tagged",
-      recordId: makeGuid(),
-      parentRecordId: "menu",
-    });
-    notEqual(query.url.href, url, "Tag query URL for existing tag should differ");
-    let params = new URLSearchParams(query.url.pathname);
-    equal(params.get("type"), "7", "Should preserve query type");
-    equal(params.get("maxResults"), "15", "Should preserve additional params");
-    equal(params.get("folder"), tagFolder, "Should update tag folder");
-    deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "Should not duplicate existing tags");
-  }
-
   info("Use the public tagging API to ensure we added the tag correctly");
   await PlacesUtils.bookmarks.insert({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
     url: "https://mozilla.org",
     title: "Mozilla",
   });
   PlacesUtils.tagging.tagURI(uri("https://mozilla.org"), ["taggy"]);
   assertURLHasTags("https://mozilla.org/", ["taggy"],
-    "Should set tags using the tagging API");
+                   "Should set tags using the tagging API");
+
+  info("Insert tag query for non existing tag");
+  {
+    let query = await PlacesSyncUtils.bookmarks.insert({
+      kind: "query",
+      recordId: makeGuid(),
+      parentRecordId: "toolbar",
+      url: "place:type=7&folder=90",
+      folder: "nonexisting",
+      title: "Tagged stuff",
+    });
+    let params = new URLSearchParams(query.url.pathname);
+    ok(!params.has("type"), "Should not preserve query type");
+    ok(!params.has("folder"), "Should not preserve folder");
+    equal(params.get("tag"), "nonexisting", "Should add tag");
+    deepEqual(PlacesUtils.tagging.allTags, ["taggy"],
+              "The nonexisting tag should not be added");
+  }
+
+  info("Insert tag query for existing tag");
+  {
+    let url = "place:type=7&folder=90&maxResults=15";
+    let query = await PlacesSyncUtils.bookmarks.insert({
+      kind: "query",
+      recordId: makeGuid(),
+      parentRecordId: "menu",
+      url,
+      folder: "taggy",
+      title: "Sorted and tagged",
+    });
+    let params = new URLSearchParams(query.url.pathname);
+    ok(!params.get("type"), "Should not preserve query type");
+    ok(!params.has("folder"), "Should not preserve folder");
+    equal(params.get("maxResults"), "15", "Should preserve additional params");
+    equal(params.get("tag"), "taggy", "Should add tag");
+    deepEqual(PlacesUtils.tagging.allTags, ["taggy"],
+              "Should not duplicate existing tags");
+  }
+
+
 
   info("Removing the tag should clean up the tag folder");
   PlacesUtils.tagging.untagURI(uri("https://mozilla.org"), null);
   deepEqual(PlacesUtils.tagging.allTags, [],
     "Should remove tag folder once last item is untagged");
 
   await PlacesUtils.bookmarks.eraseEverything();
   await PlacesSyncUtils.bookmarks.reset();
@@ -1830,21 +1830,20 @@ add_task(async function test_fetch() {
     recordId: makeGuid(),
     parentRecordId: folder.recordId,
     kind: "separator",
   });
   let tagQuery = await PlacesSyncUtils.bookmarks.insert({
     kind: "query",
     recordId: makeGuid(),
     parentRecordId: "toolbar",
-    url: "place:type=7&folder=90",
+    url: "place:tag=taggy",
     folder: "taggy",
     title: "Tagged stuff",
   });
-  let [, tagFolderId] = /\bfolder=(\d+)\b/.exec(tagQuery.url.pathname);
   let smartBmk = await PlacesSyncUtils.bookmarks.insert({
     kind: "query",
     recordId: makeGuid(),
     parentRecordId: "toolbar",
     url: "place:folder=TOOLBAR",
     query: "BookmarksToolbar",
     title: "Bookmarks toolbar query",
   });
@@ -1900,17 +1899,17 @@ add_task(async function test_fetch() {
   }
 
   info("Fetch tag query");
   {
     let item = await PlacesSyncUtils.bookmarks.fetch(tagQuery.recordId);
     deepEqual(Object.keys(item).sort(), ["recordId", "kind", "parentRecordId",
       "url", "title", "folder", "parentTitle", "dateAdded"].sort(),
       "Should include query-specific properties");
-    equal(item.url.href, `place:type=7&folder=${tagFolderId}`, "Should not rewrite outgoing tag queries");
+    equal(item.url.href, `place:tag=taggy`, "Should not rewrite outgoing tag queries");
     equal(item.folder, "taggy", "Should return tag name for tag queries");
   }
 
   info("Fetch smart bookmark");
   {
     let item = await PlacesSyncUtils.bookmarks.fetch(smartBmk.recordId);
     deepEqual(Object.keys(item).sort(), ["recordId", "kind", "parentRecordId",
       "url", "title", "query", "parentTitle", "dateAdded"].sort(),