Bug 1303825 - Track whether bookmarks are syncable in Places. f?mak draft
authorKit Cambridge <kcambridge@mozilla.com>
Tue, 20 Sep 2016 14:45:54 -0700
changeset 415717 da13996bb610cdb3e8e675f719d4b9dab9335028
parent 414722 aa1cde045429f0a37009720859996320adcbd200
child 531674 f4ed82aab11c76c1bbe6a2a8e1faf586baeddc21
push id29940
push userkcambridge@mozilla.com
push dateTue, 20 Sep 2016 22:08:27 +0000
bugs1303825
milestone51.0a1
Bug 1303825 - Track whether bookmarks are syncable in Places. f?mak MozReview-Commit-ID: F0uZ20yqmuY
services/sync/modules/engines/bookmarks.js
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/Database.cpp
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavBookmarks.h
toolkit/components/places/nsPlacesTables.h
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -453,32 +453,24 @@ BookmarksEngine.prototype = {
     for (let startIndex = 0;
          startIndex < modifiedGUIDs.length;
          startIndex += SQLITE_MAX_VARIABLE_NUMBER) {
 
       let chunkLength = Math.min(startIndex + SQLITE_MAX_VARIABLE_NUMBER,
                                  modifiedGUIDs.length);
 
       let query = `
-        WITH RECURSIVE
+        WITH
         modifiedGuids(guid) AS (
           VALUES ${new Array(chunkLength).fill("(?)").join(", ")}
-        ),
-        syncedItems(id) AS (
-          VALUES ${getChangeRootIds().map(id => `(${id})`).join(", ")}
-          UNION ALL
-          SELECT b.id
-          FROM moz_bookmarks b
-          JOIN syncedItems s ON b.parent = s.id
         )
         SELECT b.guid, b.id
         FROM modifiedGuids m
         JOIN moz_bookmarks b ON b.guid = m.guid
-        LEFT JOIN syncedItems s ON b.id = s.id
-        WHERE s.id IS NULL
+        WHERE b.syncable = 0
       `;
 
       let statement = db.createAsyncStatement(query);
       try {
         for (let i = 0; i < chunkLength; i++) {
           statement.bindByIndex(i, modifiedGUIDs[startIndex + i]);
         }
         let results = Async.querySpinningly(statement, ["id", "guid"]);
@@ -899,29 +891,17 @@ BookmarksStore.prototype = {
     }
 
     return index;
   },
 
   getAllIDs: function BStore_getAllIDs() {
     let items = {};
 
-    let query = `
-      WITH RECURSIVE
-      changeRootContents(id) AS (
-        VALUES ${getChangeRootIds().map(id => `(${id})`).join(", ")}
-        UNION ALL
-        SELECT b.id
-        FROM moz_bookmarks b
-        JOIN changeRootContents c ON b.parent = c.id
-      )
-      SELECT id, guid
-      FROM changeRootContents
-      JOIN moz_bookmarks USING (id)
-    `;
+    let query = `SELECT id, guid FROM moz_bookmarks WHERE syncable = 1`;
 
     let statement = this._getStmt(query);
     let results = Async.querySpinningly(statement, ["id", "guid"]);
     for (let { id, guid } of results) {
       let syncID = BookmarkSpecialIds.specialGUIDForId(id) || guid;
       items[syncID] = { modified: 0, deleted: false };
     }
 
@@ -1201,26 +1181,14 @@ BookmarksTracker.prototype = {
     if (--this._batchDepth === 0 && this._batchSawScoreIncrement) {
       this.score += SCORE_INCREMENT_XLARGE;
       this._batchSawScoreIncrement = false;
     }
   },
   onItemVisited: function () {}
 };
 
-// Returns an array of root IDs to recursively query for synced bookmarks.
-// Items in other roots, including tags and organizer queries, will be
-// ignored.
-function getChangeRootIds() {
-  return [
-    PlacesUtils.bookmarksMenuFolderId,
-    PlacesUtils.toolbarFolderId,
-    PlacesUtils.unfiledBookmarksFolderId,
-    PlacesUtils.mobileFolderId,
-  ];
-}
-
 class BookmarksChangeset extends Changeset {
   getModifiedTimestamp(id) {
     let change = this.changes[id];
     return change ? change.modified : Number.NaN;
   }
 }
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -817,16 +817,20 @@ function updateBookmark(info, item, newP
         // Ensure a page exists in moz_places for this URL.
         yield maybeInsertPlace(db, info.url);
         // Update tuples for the update query.
         tuples.set("url", { value: info.url.href
                           , fragment: "fk = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)" });
       }
 
       if (newParent) {
+        if (item._syncable != newParent._syncable) {
+          throw new Error("Cannot move item between synced and unsynced parents");
+        }
+
         // For simplicity, update the index regardless.
         let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
         tuples.set("position", { value: newIndex });
 
         if (newParent.guid == item.parentGuid) {
           // Moving inside the original container.
           // When moving "up", add 1 to each index in the interval.
           // Otherwise when moving down, we subtract 1.
@@ -908,19 +912,20 @@ function insertBookmark(item, parent) {
         `UPDATE moz_bookmarks SET position = position + 1
          WHERE parent = :parent
          AND position >= :index
         `, { parent: parent._id, index: item.index });
 
       // Insert the bookmark into the database.
       yield db.executeCached(
         `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
-                                    dateAdded, lastModified, guid)
+                                    dateAdded, lastModified, guid, syncable)
          VALUES ((SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url), :type, :parent,
-                 :index, :title, :date_added, :last_modified, :guid)
+                 :index, :title, :date_added, :last_modified, :guid,
+                 (SELECT syncable FROM moz_bookmarks WHERE id = :parent))
         `, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
              type: item.type, parent: parent._id, index: item.index,
              title: item.title, date_added: PlacesUtils.toPRTime(item.dateAdded),
              last_modified: PlacesUtils.toPRTime(item.lastModified), guid: item.guid });
 
       yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
     });
 
@@ -972,17 +977,18 @@ function queryBookmarks(info) {
     // hence setting them to NULL
     let rows = yield db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title,
               h.url AS url, b.parent, p.parent,
               NULL AS _id,
               NULL AS _childCount,
               NULL AS _grandParentId,
-              NULL AS _parentId
+              NULL AS _parentId,
+              NULL AS _syncable
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        ${queryString}
       `, queryParams);
 
     return rowsToItemsArray(rows);
   }));
@@ -996,17 +1002,17 @@ function fetchBookmark(info) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmark",
     Task.async(function*(db) {
 
     let rows = yield db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
               b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
+              p.parent AS _grandParentId, b.syncable AS _syncable
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE b.guid = :guid
       `, { guid: info.guid });
 
     return rows.length ? rowsToItemsArray(rows)[0] : null;
   }));
@@ -1017,17 +1023,17 @@ function fetchBookmarkByPosition(info) {
     Task.async(function*(db) {
     let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
 
     let rows = yield db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
               b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
+              p.parent AS _grandParentId, b.syncable AS _syncable
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE p.guid = :parentGuid
        AND b.position = IFNULL(:index, (SELECT count(*) - 1
                                         FROM moz_bookmarks
                                         WHERE parent = p.id))
       `, { parentGuid: info.parentGuid, index });
@@ -1041,17 +1047,17 @@ function fetchBookmarksByURL(info) {
     Task.async(function*(db) {
 
     let rows = yield db.executeCached(
       `/* do not warn (bug no): not worth to add an index */
        SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
               b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
+              p.parent AS _grandParentId, b.syncable AS _syncable
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE h.url_hash = hash(:url) AND h.url = :url
        AND _grandParentId <> :tags_folder
        ORDER BY b.lastModified DESC
       `, { url: info.url.href,
            tags_folder: PlacesUtils.tagsFolderId });
@@ -1062,17 +1068,18 @@ function fetchBookmarksByURL(info) {
 
 function fetchRecentBookmarks(numberOfItems) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchRecentBookmarks",
     Task.async(function*(db) {
 
     let rows = yield db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              NULL AS _id, NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId
+              NULL AS _id, NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId,
+              NULL AS _syncable
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE p.parent <> :tags_folder
        ORDER BY b.dateAdded DESC, b.ROWID DESC
        LIMIT :numberOfItems
       `, { tags_folder: PlacesUtils.tagsFolderId, numberOfItems });
 
@@ -1084,17 +1091,17 @@ function fetchBookmarksByParent(info) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByParent",
     Task.async(function*(db) {
 
     let rows = yield db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
               b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
               b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
-              p.parent AS _grandParentId
+              p.parent AS _grandParentId, b.syncable AS _syncable
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE p.guid = :parentGuid
        ORDER BY b.position ASC
       `, { parentGuid: info.parentGuid });
 
     return rowsToItemsArray(rows);
@@ -1288,17 +1295,17 @@ function rowsToItemsArray(rows) {
     for (let prop of ["dateAdded", "lastModified"]) {
       item[prop] = PlacesUtils.toDate(row.getResultByName(prop));
     }
     for (let prop of ["title", "parentGuid", "url" ]) {
       let val = row.getResultByName(prop);
       if (val)
         item[prop] = prop === "url" ? new URL(val) : val;
     }
-    for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId"]) {
+    for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId", "_syncable"]) {
       let val = row.getResultByName(prop);
       if (val !== null) {
         // These properties should not be returned to the API consumer, thus
         // they are non-enumerable and removed through Object.assign just before
         // the object is returned.
         // Configurable is set to support mergeIntoNewObject overwrites.
         Object.defineProperty(item, prop, { value: val, enumerable: false,
                                                         configurable: true });
@@ -1431,17 +1438,17 @@ Task.async(function* (db, folderGuids, o
          WHERE p.guid = :folderGuid
          UNION ALL
          SELECT id FROM moz_bookmarks
          JOIN descendants ON parent = did
        )
        SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
               b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
               b.lastModified, b.title, p.parent AS _grandParentId,
-              NULL AS _childCount
+              NULL AS _childCount, b.syncable AS _syncable
        FROM descendants
        JOIN moz_bookmarks b ON did = b.id
        JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON b.fk = h.id`, { folderGuid });
 
     itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
 
     yield db.executeCached(
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -247,37 +247,37 @@ SetJournalMode(nsCOMPtr<mozIStorageConne
   }
 
   return JOURNAL_DELETE;
 }
 
 nsresult
 CreateRoot(nsCOMPtr<mozIStorageConnection>& aDBConn,
            const nsCString& aRootName, const nsCString& aGuid,
-           const nsXPIDLString& titleString)
+           const nsXPIDLString& titleString, bool aSyncable = false)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // The position of the new item in its folder.
   static int32_t itemPosition = 0;
 
   // A single creation timestamp for all roots so that the root folder's
   // last modification time isn't earlier than its childrens' creation time.
   static PRTime timestamp = 0;
   if (!timestamp)
     timestamp = RoundedPRNow();
 
   // Create a new bookmark folder for the root.
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
     "INSERT INTO moz_bookmarks "
-      "(type, position, title, dateAdded, lastModified, guid, parent) "
+      "(type, position, title, dateAdded, lastModified, guid, parent, syncable) "
     "VALUES (:item_type, :item_position, :item_title,"
             ":date_added, :last_modified, :guid,"
-            "IFNULL((SELECT id FROM moz_bookmarks WHERE parent = 0), 0))"
+            "IFNULL((SELECT id FROM moz_bookmarks WHERE parent = 0), 0), :syncable)"
   ), getter_AddRefs(stmt));
   if (NS_FAILED(rv)) return rv;
 
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"),
                              nsINavBookmarksService::TYPE_FOLDER);
   if (NS_FAILED(rv)) return rv;
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_position"), itemPosition);
   if (NS_FAILED(rv)) return rv;
@@ -285,16 +285,18 @@ CreateRoot(nsCOMPtr<mozIStorageConnectio
                                   NS_ConvertUTF16toUTF8(titleString));
   if (NS_FAILED(rv)) return rv;
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), timestamp);
   if (NS_FAILED(rv)) return rv;
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), timestamp);
   if (NS_FAILED(rv)) return rv;
   rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGuid);
   if (NS_FAILED(rv)) return rv;
+  rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("syncable"), aSyncable);
+  if (NS_FAILED(rv)) return rv;
   rv = stmt->Execute();
   if (NS_FAILED(rv)) return rv;
 
   // The 'places' root is a folder containing the other roots.
   // The first bookmark in a folder has position 0.
   if (!aRootName.EqualsLiteral("places"))
     ++itemPosition;
 
@@ -997,38 +999,41 @@ Database::CreateBookmarkRoots()
                   NS_LITERAL_CSTRING("root________"), rootTitle);
   if (NS_FAILED(rv)) return rv;
 
   // Fetch the internationalized folder name from the string bundle.
   rv = bundle->GetStringFromName(u"BookmarksMenuFolderTitle",
                                  getter_Copies(rootTitle));
   if (NS_FAILED(rv)) return rv;
   rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("menu"),
-                  NS_LITERAL_CSTRING("menu________"), rootTitle);
+                  NS_LITERAL_CSTRING("menu________"), rootTitle,
+                  /* aSyncable */ true);
   if (NS_FAILED(rv)) return rv;
 
   rv = bundle->GetStringFromName(u"BookmarksToolbarFolderTitle",
                                  getter_Copies(rootTitle));
   if (NS_FAILED(rv)) return rv;
   rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("toolbar"),
-                  NS_LITERAL_CSTRING("toolbar_____"), rootTitle);
+                  NS_LITERAL_CSTRING("toolbar_____"), rootTitle,
+                  /* aSyncable */ true);
   if (NS_FAILED(rv)) return rv;
 
   rv = bundle->GetStringFromName(u"TagsFolderTitle",
                                  getter_Copies(rootTitle));
   if (NS_FAILED(rv)) return rv;
   rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("tags"),
                   NS_LITERAL_CSTRING("tags________"), rootTitle);
   if (NS_FAILED(rv)) return rv;
 
   rv = bundle->GetStringFromName(u"OtherBookmarksFolderTitle",
                                  getter_Copies(rootTitle));
   if (NS_FAILED(rv)) return rv;
   rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("unfiled"),
-                  NS_LITERAL_CSTRING("unfiled_____"), rootTitle);
+                  NS_LITERAL_CSTRING("unfiled_____"), rootTitle,
+                  /* aSyncable */ true);
   if (NS_FAILED(rv)) return rv;
 
   rv = CreateMobileRoot();
   if (NS_FAILED(rv)) return rv;
 
 #if DEBUG
   nsCOMPtr<mozIStorageStatement> stmt;
   rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
@@ -1936,16 +1941,45 @@ Database::MigrateV35Up() {
     rv = stmt->Execute();
     if (NS_FAILED(rv)) return rv;
   }
 
   // Clean up orphan annotations for the removed folders.
   rv = RemoveOrphanAnnotations();
   if (NS_FAILED(rv)) return rv;
 
+  // Add a column indicating whether a bookmark is syncable.
+  nsCOMPtr<mozIStorageStatement> syncableStmt;
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT syncable FROM moz_bookmarks"
+  ), getter_AddRefs(syncableStmt));
+  if (NS_FAILED(rv)) {
+    rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "ALTER TABLE moz_bookmarks "
+      "ADD COLUMN syncable INTEGER DEFAULT 0 NOT NULL"
+    ));
+    NS_ENSURE_SUCCESS(rv, rv);
+    printf("KITCAM: OK, SHOULD HAVE CREATED!\n");
+  }
+
+  // Mark the menu, toolbar, unfiled, and mobile roots, and their descendants,
+  // as syncable.
+  rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "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 "
+    ") "
+    "UPDATE moz_bookmarks SET syncable = 1 WHERE id IN syncedItems"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
 nsresult
 Database::CreateMobileRoot()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -1963,23 +1997,23 @@ Database::CreateMobileRoot()
   if (NS_FAILED(rv)) return rv;
 
   // Create the mobile root, ignoring conflicts if one already exists (for
   // example, if the user downgraded to an earlier release channel).
   {
     nsCOMPtr<mozIStorageStatement> stmt;
     nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
       "INSERT OR IGNORE INTO moz_bookmarks "
-        "(type, position, title, dateAdded, lastModified, guid, parent) "
+        "(type, position, title, dateAdded, lastModified, guid, parent, syncable) "
       "VALUES (:item_type, "
                "(SELECT COUNT(*) FROM moz_bookmarks b "
                 "JOIN moz_bookmarks p ON p.id = b.parent "
                 "WHERE p.parent = 0), "
                ":item_title, :timestamp, :timestamp, :guid, "
-               "(SELECT id FROM moz_bookmarks WHERE parent = 0))"
+               "(SELECT id FROM moz_bookmarks WHERE parent = 0), 1)"
     ), getter_AddRefs(stmt));
     if (NS_FAILED(rv)) return rv;
     mozStorageStatementScoper scoper(stmt);
 
     rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"),
                                nsINavBookmarksService::TYPE_FOLDER);
     if (NS_FAILED(rv)) return rv;
     rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -351,20 +351,20 @@ nsNavBookmarks::InsertBookmarkInDB(int64
   // Check for a valid itemId.
   MOZ_ASSERT(_itemId && (*_itemId == -1 || *_itemId > 0));
   // Check for a valid placeId.
   MOZ_ASSERT(aPlaceId && (aPlaceId == -1 || aPlaceId > 0));
 
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "INSERT INTO moz_bookmarks "
       "(id, fk, type, parent, position, title, "
-       "dateAdded, lastModified, guid) "
+       "dateAdded, lastModified, guid, syncable) "
     "VALUES (:item_id, :page_id, :item_type, :parent, :item_index, "
             ":item_title, :date_added, :last_modified, "
-            ":item_guid)"
+            ":item_guid, (SELECT syncable FROM moz_bookmarks WHERE id = :parent))"
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
   nsresult rv;
   if (*_itemId != -1)
     rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), *_itemId);
   else
@@ -1214,21 +1214,27 @@ nsNavBookmarks::MoveItem(int64_t aItemId
       rv = GetFolderIdForItem(ancestorId, &ancestorId);
       if (NS_FAILED(rv)) {
         break;
       }
     }
   }
 
   // calculate new index
-  int32_t newIndex, folderCount;
+  int32_t newIndex, folderCount, newParentSyncable;
   int64_t grandParentId;
   nsAutoCString newParentGuid;
-  rv = FetchFolderInfo(aNewParent, &folderCount, newParentGuid, &grandParentId);
+  rv = FetchFolderInfo(aNewParent, &folderCount, newParentGuid, &grandParentId,
+                       &newParentSyncable);
   NS_ENSURE_SUCCESS(rv, rv);
+
+  if (bookmark.syncable != newParentSyncable) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
   if (aIndex == nsINavBookmarksService::DEFAULT_INDEX ||
       aIndex >= folderCount) {
     newIndex = folderCount;
     // If the parent remains the same, then the folder is really being moved
     // to count - 1 (since it's being removed from the old position)
     if (bookmark.parentId == aNewParent) {
       --newIndex;
     }
@@ -1321,17 +1327,17 @@ nsNavBookmarks::MoveItem(int64_t aItemId
 
 nsresult
 nsNavBookmarks::FetchItemInfo(int64_t aItemId,
                               BookmarkData& _bookmark)
 {
   // LEFT JOIN since not all bookmarks have an associated place.
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "SELECT b.id, h.url, b.title, b.position, b.fk, b.parent, b.type, "
-           "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent "
+           "b.dateAdded, b.lastModified, b.guid, t.guid, t.parent, b.syncable "
     "FROM moz_bookmarks b "
     "LEFT JOIN moz_bookmarks t ON t.id = b.parent "
     "LEFT JOIN moz_places h ON h.id = b.fk "
     "WHERE b.id = :item_id"
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
@@ -1379,16 +1385,18 @@ nsNavBookmarks::FetchItemInfo(int64_t aI
     rv = stmt->GetUTF8String(10, _bookmark.parentGuid);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->GetInt64(11, &_bookmark.grandParentId);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   else {
     _bookmark.grandParentId = -1;
   }
+  rv = stmt->GetInt32(12, &_bookmark.syncable);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 nsresult
 nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
                                     int64_t aItemId,
                                     PRTime aValue)
@@ -1834,27 +1842,29 @@ nsNavBookmarks::QueryFolderChildrenAsync
   return NS_OK;
 }
 
 
 nsresult
 nsNavBookmarks::FetchFolderInfo(int64_t aFolderId,
                                 int32_t* _folderCount,
                                 nsACString& _guid,
-                                int64_t* _parentId)
+                                int64_t* _parentId,
+                                int32_t* _syncable)
 {
   *_folderCount = 0;
   *_parentId = -1;
 
   // This query has to always return results, so it can't be written as a join,
   // though a left join of 2 subqueries would have the same cost.
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
     "SELECT count(*), "
             "(SELECT guid FROM moz_bookmarks WHERE id = :parent), "
-            "(SELECT parent FROM moz_bookmarks WHERE id = :parent) "
+            "(SELECT parent FROM moz_bookmarks WHERE id = :parent), "
+            "(SELECT syncable FROM moz_bookmarks WHERE id = :parent) "
     "FROM moz_bookmarks "
     "WHERE parent = :parent"
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
   nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aFolderId);
   NS_ENSURE_SUCCESS(rv, rv);
@@ -1873,16 +1883,20 @@ nsNavBookmarks::FetchFolderInfo(int64_t 
 
   rv = stmt->GetInt32(0, _folderCount);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!isNull) {
     rv = stmt->GetUTF8String(1, _guid);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->GetInt64(2, _parentId);
     NS_ENSURE_SUCCESS(rv, rv);
+    if (_syncable) {
+      rv = stmt->GetInt32(3, _syncable);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
   }
 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::IsBookmarked(nsIURI* aURI, bool* aBookmarked)
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -36,16 +36,17 @@ namespace places {
     int64_t parentId;
     int64_t grandParentId;
     int32_t type;
     nsCString serviceCID;
     PRTime dateAdded;
     PRTime lastModified;
     nsCString guid;
     nsCString parentGuid;
+    int32_t syncable;
   };
 
   struct ItemVisitData {
     BookmarkData bookmark;
     int64_t visitId;
     uint32_t transitionType;
     PRTime time;
   };
@@ -253,17 +254,18 @@ private:
    * @param _parentId
    *        Id of the parent of the folder.
    *
    * @throws If folder does not exist.
    */
   nsresult FetchFolderInfo(int64_t aFolderId,
                            int32_t* _folderCount,
                            nsACString& _guid,
-                           int64_t* _parentId);
+                           int64_t* _parentId,
+                           int32_t* syncable = nullptr);
 
   nsresult GetLastChildId(int64_t aFolder, int64_t* aItemId);
 
   /**
    * This is an handle to the Places database.
    */
   RefPtr<mozilla::places::Database> mDB;
 
--- a/toolkit/components/places/nsPlacesTables.h
+++ b/toolkit/components/places/nsPlacesTables.h
@@ -102,16 +102,17 @@
     ", parent INTEGER" \
     ", position INTEGER" \
     ", title LONGVARCHAR" \
     ", keyword_id INTEGER" \
     ", folder_type TEXT" \
     ", dateAdded INTEGER" \
     ", lastModified INTEGER" \
     ", guid TEXT" \
+    ", syncable INTEGER NOT NULL DEFAULT 0" \
   ")" \
 )
 
 #define CREATE_MOZ_KEYWORDS NS_LITERAL_CSTRING( \
   "CREATE TABLE moz_keywords (" \
     "  id INTEGER PRIMARY KEY AUTOINCREMENT" \
     ", keyword TEXT UNIQUE" \
     ", place_id INTEGER" \