--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -874,17 +874,18 @@ SyncEngine.prototype = {
* replace the sync ID in `meta/global` with the assigned ID.
*/
async ensureCurrentSyncID(newSyncID) {
let existingSyncID = this._syncID;
if (existingSyncID == newSyncID) {
return existingSyncID;
}
this._log.debug("Engine syncIDs: " + [newSyncID, existingSyncID]);
- this.setSyncIDPref(newSyncID);
+ Svc.Prefs.set(this.name + ".syncID", newSyncID);
+ Svc.Prefs.set(this.name + ".lastSync", "0");
return newSyncID;
},
/**
* Resets the local sync ID for the engine, wipes the server, and resets all
* local Sync state to start over as a first sync.
*
* @return the new sync ID.
@@ -897,23 +898,17 @@ SyncEngine.prototype = {
/**
* Resets the local sync ID for the engine, signaling that we're starting over
* as a first sync.
*
* @return the new sync ID.
*/
async resetLocalSyncID() {
- return this.setSyncIDPref(Utils.makeGUID());
- },
-
- setSyncIDPref(syncID) {
- Svc.Prefs.set(this.name + ".syncID", syncID);
- Svc.Prefs.set(this.name + ".lastSync", "0");
- return syncID;
+ return this.ensureCurrentSyncID(Utils.makeGUID());
},
/*
* lastSync is a timestamp in server time.
*/
async getLastSync() {
return this._lastSync;
},
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -329,41 +329,47 @@ BaseBookmarksEngine.prototype = {
let shouldWipeRemote = await PlacesSyncUtils.bookmarks.shouldWipeRemote();
if (!shouldWipeRemote) {
// Migrate the bookmarks sync ID and last sync time from prefs, to avoid
// triggering a full sync on upgrade. This can be removed in bug 1443021.
let existingSyncID = await super.getSyncID();
if (existingSyncID) {
this._log.debug("Migrating existing sync ID ${existingSyncID} from " +
"prefs", { existingSyncID });
- await PlacesSyncUtils.bookmarks.ensureCurrentSyncId(existingSyncID);
+ await this._ensureCurrentSyncID(existingSyncID);
}
if (migrateLastSync) {
let existingLastSync = await super.getLastSync();
if (existingLastSync) {
this._log.debug("Migrating existing last sync time " +
"${existingLastSync} from prefs",
{ existingLastSync });
await PlacesSyncUtils.bookmarks.setLastSync(existingLastSync);
}
}
}
this._migratedSyncMetadata = true;
},
+ // Exposed so that the buffered engine can override to store the sync ID in
+ // the mirror.
+ _ensureCurrentSyncID(newSyncID) {
+ return PlacesSyncUtils.bookmarks.ensureCurrentSyncId(newSyncID);
+ },
+
async ensureCurrentSyncID(newSyncID) {
let shouldWipeRemote = await PlacesSyncUtils.bookmarks.shouldWipeRemote();
if (!shouldWipeRemote) {
this._log.debug("Checking if server sync ID ${newSyncID} matches " +
"existing", { newSyncID });
- await PlacesSyncUtils.bookmarks.ensureCurrentSyncId(newSyncID);
+ await this._ensureCurrentSyncID(newSyncID);
// Update the sync ID in prefs to allow downgrading to older Firefox
// releases that don't store Sync metadata in Places. This can be removed
// in bug 1443021.
- super.setSyncIDPref(newSyncID);
+ await super.ensureCurrentSyncID(newSyncID);
return newSyncID;
}
// We didn't take the new sync ID because we need to wipe the server
// and other clients after a restore. Send the command, wipe the
// server, and reset our sync ID to reupload everything.
this._log.debug("Ignoring server sync ID ${newSyncID} after restore; " +
"wiping server and resetting sync ID", { newSyncID });
await this.service.clientsEngine.sendCommand("wipeEngine", [this.name],
@@ -375,24 +381,20 @@ BaseBookmarksEngine.prototype = {
async resetSyncID() {
await this._deleteServerCollection();
return this.resetLocalSyncID();
},
async resetLocalSyncID() {
let newSyncID = await PlacesSyncUtils.bookmarks.resetSyncId();
this._log.debug("Assigned new sync ID ${newSyncID}", { newSyncID });
- super.setSyncIDPref(newSyncID); // Remove in bug 1443021.
+ await super.ensureCurrentSyncID(newSyncID); // Remove in bug 1443021.
return newSyncID;
},
- setSyncIDPref(syncID) {
- throw new Error("Use ensureCurrentSyncID or resetLocalSyncID");
- },
-
async _syncFinish() {
await SyncEngine.prototype._syncFinish.call(this);
await PlacesSyncUtils.bookmarks.ensureMobileQuery();
},
async _createRecord(id) {
if (this._modified.isTombstone(id)) {
// If we already know a changed item is a tombstone, just create the
@@ -417,16 +419,20 @@ BaseBookmarksEngine.prototype = {
let changes = this._modified.changes;
await PlacesSyncUtils.bookmarks.pushChanges(changes);
},
_deleteId(id) {
this._noteDeletedId(id);
},
+ // The bookmarks engine rarely calls this method directly, except in tests or
+ // when handling a `reset{All, Engine}` command from another client. We
+ // usually reset local Sync metadata on a sync ID mismatch, which both engines
+ // override with logic that lives in Places and the mirror.
async _resetClient() {
await super._resetClient();
await PlacesSyncUtils.bookmarks.reset();
},
// Cleans up the Places root, reading list items (ignored in bug 762118,
// removed in bug 1155684), and pinned sites.
_shouldDeleteRemotely(incomingItem) {
@@ -777,24 +783,30 @@ BufferedBookmarksEngine.prototype = {
async _syncStartup() {
await this._migrateSyncMetadata({
migrateLastSync: false,
});
await super._syncStartup();
},
+ async _ensureCurrentSyncID(newSyncID) {
+ await super._ensureCurrentSyncID(newSyncID);
+ let buf = await this._store.ensureOpenMirror();
+ await buf.ensureCurrentSyncId(newSyncID);
+ },
+
async getSyncID() {
return PlacesSyncUtils.bookmarks.getSyncId();
},
async resetLocalSyncID() {
+ let newSyncID = await super.resetLocalSyncID();
let buf = await this._store.ensureOpenMirror();
- await buf.reset();
- let newSyncID = await super.resetLocalSyncID();
+ await buf.ensureCurrentSyncId(newSyncID);
return newSyncID;
},
async getLastSync() {
let mirror = await this._store.ensureOpenMirror();
return mirror.getCollectionHighWaterMark();
},
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -70,40 +70,36 @@ HistoryEngine.prototype = {
async getSyncID() {
return PlacesSyncUtils.history.getSyncId();
},
async ensureCurrentSyncID(newSyncID) {
this._log.debug("Checking if server sync ID ${newSyncID} matches existing",
{ newSyncID });
await PlacesSyncUtils.history.ensureCurrentSyncId(newSyncID);
- super.setSyncIDPref(newSyncID); // Remove in bug 1443021.
+ await super.ensureCurrentSyncID(newSyncID); // Remove in bug 1443021.
return newSyncID;
},
async resetSyncID() {
// First, delete the collection on the server. It's fine if we're
// interrupted here: on the next sync, we'll detect that our old sync ID is
// now stale, and start over as a first sync.
await this._deleteServerCollection();
// Then, reset our local sync ID.
return this.resetLocalSyncID();
},
async resetLocalSyncID() {
let newSyncID = await PlacesSyncUtils.history.resetSyncId();
this._log.debug("Assigned new sync ID ${newSyncID}", { newSyncID });
- await super.setSyncIDPref(newSyncID); // Remove in bug 1443021.
+ await super.ensureCurrentSyncID(newSyncID); // Remove in bug 1443021.
return newSyncID;
},
- setSyncIDPref(syncID) {
- throw new Error("Use ensureCurrentSyncID or resetLocalSyncID");
- },
-
async getLastSync() {
let lastSync = await PlacesSyncUtils.history.getLastSync();
return lastSync;
},
async setLastSync(lastSync) {
await PlacesSyncUtils.history.setLastSync(lastSync);
await super.setLastSync(lastSync); // Remove in bug 1443021.
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -1,13 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
ChromeUtils.import("resource://gre/modules/BookmarkJSONUtils.jsm");
+ChromeUtils.import("resource://gre/modules/SyncedBookmarksMirror.jsm");
ChromeUtils.import("resource://gre/modules/Log.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");
ChromeUtils.import("resource://services-common/utils.js");
ChromeUtils.import("resource://services-sync/constants.js");
ChromeUtils.import("resource://services-sync/engines.js");
ChromeUtils.import("resource://services-sync/engines/bookmarks.js");
ChromeUtils.import("resource://services-sync/service.js");
ChromeUtils.import("resource://services-sync/util.js");
@@ -1093,8 +1094,109 @@ add_task(async function test_buffered_mi
equal(Svc.Prefs.get(`${bufferedEngine.name}.syncID`), newSyncID,
"Changing buffered engine sync ID should update prefs");
strictEqual(Svc.Prefs.get(`${bufferedEngine.name}.lastSync`), "0",
"Changing buffered engine sync ID should clear last sync pref");
await bufferedEngine.wipeClient();
});
+
+// The buffered engine stores the sync ID and last sync time in three places:
+// prefs, Places, and the mirror. We can remove the prefs entirely in bug
+// 1443021, and drop the last sync time from Places once we remove the legacy
+// engine. This test ensures we keep them in sync (^_^), and handle mismatches
+// in case the user copies Places or the mirror between accounts. See
+// bug 1199077, comment 84 for the gory details.
+add_task(async function test_mirror_syncID() {
+ let bufferedEngine = new BufferedBookmarksEngine(Service);
+ await bufferedEngine.initialize();
+ let buf = await bufferedEngine._store.ensureOpenMirror();
+
+ info("Places and mirror don't have sync IDs");
+
+ let syncID = await bufferedEngine.resetLocalSyncID();
+
+ equal(Svc.Prefs.get(`${bufferedEngine.name}.syncID`), syncID,
+ "Should reset sync ID in prefs");
+ strictEqual(Svc.Prefs.get(`${bufferedEngine.name}.lastSync`), "0",
+ "Should reset last sync in prefs");
+
+ equal(await PlacesSyncUtils.bookmarks.getSyncId(), syncID,
+ "Should reset sync ID in Places");
+ strictEqual(await PlacesSyncUtils.bookmarks.getLastSync(), 0,
+ "Should reset last sync in Places");
+
+ equal(await buf.getSyncId(), syncID, "Should reset sync ID in mirror");
+ strictEqual(await buf.getCollectionHighWaterMark(), 0,
+ "Should reset high water mark in mirror");
+
+ info("Places and mirror have matching sync ID");
+
+ await bufferedEngine.setLastSync(123.45);
+ await bufferedEngine.ensureCurrentSyncID(syncID);
+
+ equal(Svc.Prefs.get(`${bufferedEngine.name}.syncID`), syncID,
+ "Should keep sync ID in prefs if Places and mirror match");
+ strictEqual(Svc.Prefs.get(`${bufferedEngine.name}.lastSync`), "123.45",
+ "Should keep last sync in prefs if Places and mirror match");
+
+ equal(await PlacesSyncUtils.bookmarks.getSyncId(), syncID,
+ "Should keep sync ID in Places if Places and mirror match");
+ strictEqual(await PlacesSyncUtils.bookmarks.getLastSync(), 123.45,
+ "Should keep last sync in Places if Places and mirror match");
+
+ equal(await buf.getSyncId(), syncID, "Should keep sync ID in mirror");
+ equal(await buf.getCollectionHighWaterMark(), 123.45,
+ "Should keep high water mark in mirror");
+
+ info("Places and mirror have different sync IDs");
+
+ // Directly update the sync ID in the mirror, without resetting.
+ await buf.db.execute(`UPDATE meta SET value = :value WHERE key = :key`,
+ { key: SyncedBookmarksMirror.META_KEY.SYNC_ID,
+ value: "syncIdAAAAAA" });
+ await bufferedEngine.ensureCurrentSyncID(syncID);
+
+ equal(Svc.Prefs.get(`${bufferedEngine.name}.syncID`), syncID,
+ "Should keep sync ID in prefs if Places and mirror don't match");
+ strictEqual(Svc.Prefs.get(`${bufferedEngine.name}.lastSync`), "123.45",
+ "Should keep last sync in prefs if Places and mirror don't match");
+
+ equal(await PlacesSyncUtils.bookmarks.getSyncId(), syncID,
+ "Should keep existing sync ID in Places on mirror sync ID mismatch");
+ strictEqual(await PlacesSyncUtils.bookmarks.getLastSync(), 123.45,
+ "Should keep existing last sync in Places on mirror sync ID mismatch");
+
+ equal(await buf.getSyncId(), syncID,
+ "Should reset mismatched sync ID in mirror");
+ strictEqual(await buf.getCollectionHighWaterMark(), 0,
+ "Should reset high water mark on mirror sync ID mismatch");
+
+ info("Places has sync ID; mirror missing sync ID");
+ await buf.reset();
+
+ equal(await bufferedEngine.ensureCurrentSyncID(syncID), syncID,
+ "Should not assign new sync ID if Places has sync ID; mirror missing");
+ equal(await buf.getSyncId(), syncID,
+ "Should set sync ID in mirror to match Places");
+
+ info("Places missing sync ID; mirror has sync ID");
+
+ await buf.setCollectionLastModified(123.45);
+ await PlacesSyncUtils.bookmarks.reset();
+ let newSyncID = await bufferedEngine.ensureCurrentSyncID("syncIdBBBBBB");
+
+ equal(Svc.Prefs.get(`${bufferedEngine.name}.syncID`), newSyncID,
+ "Should set new sync ID in prefs");
+ strictEqual(Svc.Prefs.get(`${bufferedEngine.name}.lastSync`), "0",
+ "Should reset last sync in prefs on sync ID change");
+
+ equal(await PlacesSyncUtils.bookmarks.getSyncId(), newSyncID,
+ "Should set new sync ID in Places");
+ equal(await buf.getSyncId(), newSyncID,
+ "Should update new sync ID in mirror");
+
+ strictEqual(await buf.getCollectionHighWaterMark(), 0,
+ "Should reset high water mark on sync ID change in Places");
+
+ await bufferedEngine.wipeClient();
+});
--- a/toolkit/components/places/PlacesSyncUtils.jsm
+++ b/toolkit/components/places/PlacesSyncUtils.jsm
@@ -122,23 +122,23 @@ const HistorySyncUtils = PlacesSyncUtils
}
await PlacesUtils.withConnectionWrapper(
"HistorySyncUtils: ensureCurrentSyncId",
async function(db) {
let existingSyncId = await PlacesUtils.metadata.getWithConnection(
db, HistorySyncUtils.SYNC_ID_META_KEY);
if (existingSyncId == newSyncId) {
- HistorySyncLog.debug("History sync ID up-to-date",
+ HistorySyncLog.trace("History sync ID up-to-date",
{ existingSyncId });
return;
}
- HistorySyncLog.debug("History sync ID changed; resetting metadata",
- { existingSyncId, newSyncId });
+ HistorySyncLog.info("History sync ID changed; resetting metadata",
+ { existingSyncId, newSyncId });
await db.executeTransaction(function() {
return setHistorySyncId(db, newSyncId);
});
}
);
},
/**
@@ -470,33 +470,33 @@ const BookmarkSyncUtils = PlacesSyncUtil
"BookmarkSyncUtils: ensureCurrentSyncId",
async function(db) {
let existingSyncId = await PlacesUtils.metadata.getWithConnection(
db, BookmarkSyncUtils.SYNC_ID_META_KEY);
// If we don't have a sync ID, take the server's without resetting
// sync statuses.
if (!existingSyncId) {
- BookmarkSyncLog.debug("Taking new bookmarks sync ID", { newSyncId });
+ BookmarkSyncLog.info("Taking new bookmarks sync ID", { newSyncId });
await db.executeTransaction(() => setBookmarksSyncId(db, newSyncId));
return;
}
// If the existing sync ID matches the server, great!
if (existingSyncId == newSyncId) {
- BookmarkSyncLog.debug("Bookmarks sync ID up-to-date",
+ BookmarkSyncLog.trace("Bookmarks sync ID up-to-date",
{ existingSyncId });
return;
}
// Otherwise, we have a sync ID, but it doesn't match, so we were likely
// node reassigned. Take the server's sync ID and reset all items to
// "UNKNOWN" so that we can merge.
- BookmarkSyncLog.debug("Bookmarks sync ID changed; resetting sync " +
- "statuses", { existingSyncId, newSyncId });
+ BookmarkSyncLog.info("Bookmarks sync ID changed; resetting sync " +
+ "statuses", { existingSyncId, newSyncId });
await db.executeTransaction(async function() {
await setBookmarksSyncId(db, newSyncId);
await resetAllSyncStatuses(db,
PlacesUtils.bookmarks.SYNC_STATUS.UNKNOWN);
});
}
);
},
--- a/toolkit/components/places/SyncedBookmarksMirror.jsm
+++ b/toolkit/components/places/SyncedBookmarksMirror.jsm
@@ -204,23 +204,23 @@ class SyncedBookmarksMirror {
* The high water mark time, in seconds.
*/
async getCollectionHighWaterMark() {
// The first case, where we have records with server modified times newer
// than the collection last modified time, occurs when a sync is interrupted
// before we call `setCollectionLastModified`. We subtract one second, the
// maximum time precision guaranteed by the server, so that we don't miss
// other records with the same time as the newest one we downloaded.
- let rows = await this.db.execute(`
+ let rows = await this.db.executeCached(`
SELECT MAX(
IFNULL((SELECT MAX(serverModified) - 1000 FROM items), 0),
IFNULL((SELECT CAST(value AS INTEGER) FROM meta
WHERE key = :modifiedKey), 0)
) AS highWaterMark`,
- { modifiedKey: SyncedBookmarksMirror.META.MODIFIED });
+ { modifiedKey: SyncedBookmarksMirror.META_KEY.LAST_MODIFIED });
let highWaterMark = rows[0].getResultByName("highWaterMark");
return highWaterMark / 1000;
}
/**
* Updates the bookmarks collection last modified time. Note that this may
* be newer than the modified time of the most recent record.
*
@@ -229,20 +229,73 @@ class SyncedBookmarksMirror {
*/
async setCollectionLastModified(lastModifiedSeconds) {
let lastModified = Math.floor(lastModifiedSeconds * 1000);
if (!Number.isInteger(lastModified)) {
throw new TypeError("Invalid collection last modified time");
}
await this.db.executeBeforeShutdown(
"SyncedBookmarksMirror: setCollectionLastModified",
- db => db.execute(`
+ db => db.executeCached(`
REPLACE INTO meta(key, value)
VALUES(:modifiedKey, :lastModified)`,
- { modifiedKey: SyncedBookmarksMirror.META.MODIFIED, lastModified })
+ { modifiedKey: SyncedBookmarksMirror.META_KEY.LAST_MODIFIED,
+ lastModified })
+ );
+ }
+
+ /**
+ * Returns the bookmarks collection sync ID. This corresponds to
+ * `PlacesSyncUtils.bookmarks.getSyncId`.
+ *
+ * @return {String}
+ * The sync ID, or `""` if one isn't set.
+ */
+ async getSyncId() {
+ let rows = await this.db.executeCached(`
+ SELECT value FROM meta WHERE key = :syncIdKey`,
+ { syncIdKey: SyncedBookmarksMirror.META_KEY.SYNC_ID });
+ return rows.length ? rows[0].getResultByName("value") : "";
+ }
+
+ /**
+ * Ensures that the sync ID in the mirror is up-to-date with the server and
+ * Places, and discards the mirror on mismatch.
+ *
+ * The bookmarks engine store the same sync ID in Places and the mirror to
+ * "tie" the two together. This allows Sync to do the right thing if the
+ * database files are copied between profiles connected to different accounts.
+ *
+ * See `PlacesSyncUtils.bookmarks.ensureCurrentSyncId` for an explanation of
+ * how Places handles sync ID mismatches.
+ *
+ * @param {String} newSyncId
+ * The server's sync ID.
+ */
+ async ensureCurrentSyncId(newSyncId) {
+ if (!newSyncId || typeof newSyncId != "string") {
+ throw new TypeError("Invalid new bookmarks sync ID");
+ }
+ let existingSyncId = await this.getSyncId();
+ if (existingSyncId == newSyncId) {
+ MirrorLog.trace("Sync ID up-to-date in mirror", { existingSyncId });
+ return;
+ }
+ MirrorLog.info("Sync ID changed from ${existingSyncId} to " +
+ "${newSyncId}; resetting mirror",
+ { existingSyncId, newSyncId });
+ await this.db.executeBeforeShutdown(
+ "SyncedBookmarksMirror: ensureCurrentSyncId",
+ db => db.executeTransaction(async function() {
+ await resetMirror(db);
+ await db.execute(`
+ REPLACE INTO meta(key, value)
+ VALUES(:syncIdKey, :newSyncId)`,
+ { syncIdKey: SyncedBookmarksMirror.META_KEY.SYNC_ID, newSyncId });
+ })
);
}
/**
* Stores incoming or uploaded Sync records in the mirror. Rejects if any
* records are invalid.
*
* @param {PlacesItem[]} records
@@ -475,28 +528,17 @@ class SyncedBookmarksMirror {
/**
* Discards the mirror contents. This is called when the user is node
* reassigned, disables the bookmarks engine, or signs out.
*/
async reset() {
await this.db.executeBeforeShutdown(
"SyncedBookmarksMirror: reset",
- async function(db) {
- await db.executeTransaction(async function() {
- await db.execute(`DELETE FROM meta`);
- await db.execute(`DELETE FROM structure`);
- await db.execute(`DELETE FROM items`);
- await db.execute(`DELETE FROM urls`);
-
- // Since we need to reset the modified times for the syncable roots,
- // we simply delete and recreate them.
- await createMirrorRoots(db);
- });
- }
+ db => db.executeTransaction(() => resetMirror(db))
);
}
/**
* Fetches the GUIDs of all items in the remote tree that need to be merged
* into the local tree.
*
* @return {String[]}
@@ -1893,19 +1935,20 @@ this.SyncedBookmarksMirror = SyncedBookm
SyncedBookmarksMirror.KIND = {
BOOKMARK: 1,
QUERY: 2,
FOLDER: 3,
LIVEMARK: 4,
SEPARATOR: 5,
};
-/** Valid key types for the key-value `meta` table. */
-SyncedBookmarksMirror.META = {
- MODIFIED: 1,
+/** Key names for the key-value `meta` table. */
+SyncedBookmarksMirror.META_KEY = {
+ LAST_MODIFIED: "collection/lastModified",
+ SYNC_ID: "collection/syncId",
};
/**
* An error thrown when the merge can't proceed because the local or remote
* tree is inconsistent.
*/
SyncedBookmarksMirror.ConsistencyError =
class ConsistencyError extends Error {};
@@ -1938,22 +1981,21 @@ async function migrateMirrorSchema(db) {
/**
* Initializes a new mirror database, creating persistent tables, indexes, and
* roots.
*
* @param {Sqlite.OpenedConnection} db
* The mirror database connection.
*/
async function initializeMirrorDatabase(db) {
- // Key-value metadata table. Currently stores just the server collection
- // last modified time.
+ // Key-value metadata table. Stores the server collection last modified time
+ // and sync ID.
await db.execute(`CREATE TABLE mirror.meta(
- key INTEGER PRIMARY KEY,
+ key TEXT PRIMARY KEY,
value NOT NULL
- CHECK(key = ${SyncedBookmarksMirror.META.MODIFIED})
)`);
await db.execute(`CREATE TABLE mirror.items(
id INTEGER PRIMARY KEY,
guid TEXT UNIQUE NOT NULL,
/* The server modified time, in milliseconds. */
serverModified INTEGER NOT NULL DEFAULT 0,
needsMerge BOOLEAN NOT NULL DEFAULT 0,
@@ -2614,16 +2656,27 @@ async function initializeTempMirrorEntit
await db.execute(`CREATE TEMP TABLE structureToUpload(
guid TEXT PRIMARY KEY,
parentId INTEGER NOT NULL REFERENCES itemsToUpload(id)
ON DELETE CASCADE,
position INTEGER NOT NULL
) WITHOUT ROWID`);
}
+async function resetMirror(db) {
+ await db.execute(`DELETE FROM meta`);
+ await db.execute(`DELETE FROM structure`);
+ await db.execute(`DELETE FROM items`);
+ await db.execute(`DELETE FROM urls`);
+
+ // Since we need to reset the modified times and merge flags for the syncable
+ // roots, we simply delete and recreate them.
+ await createMirrorRoots(db);
+}
+
// Converts a Sync record ID to a Places GUID. Returns `null` if the ID is
// invalid.
function validateGuid(recordId) {
let guid = PlacesSyncUtils.bookmarks.recordIdToGuid(recordId);
return PlacesUtils.isValidGuid(guid) ? guid : null;
}
// Converts a Sync record's last modified time to milliseconds.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_bookmark_mirror_meta.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_highWaterMark() {
+ let buf = await openMirror("highWaterMark");
+
+ strictEqual(await buf.getCollectionHighWaterMark(), 0,
+ "High water mark should be 0 without items");
+
+ await buf.setCollectionLastModified(123.45);
+ equal(await buf.getCollectionHighWaterMark(), 123.45,
+ "High water mark should be last modified time without items");
+
+ await buf.store([{
+ id: "menu",
+ type: "folder",
+ children: [],
+ modified: 50,
+ }, {
+ id: "toolbar",
+ type: "folder",
+ children: [],
+ modified: 123.95,
+ }]);
+ equal(await buf.getCollectionHighWaterMark(), 123.45,
+ "High water mark should be last modified time if items are older");
+
+ await buf.store([{
+ id: "unfiled",
+ type: "folder",
+ children: [],
+ modified: 125.45,
+ }]);
+ equal(await buf.getCollectionHighWaterMark(), 124.45,
+ "High water mark should be modified time - 1s of newest record if exists");
+
+ await buf.finalize();
+});
+
+add_task(async function test_ensureCurrentSyncId() {
+ let buf = await openMirror("ensureCurrentSyncId");
+
+ await buf.ensureCurrentSyncId("syncIdAAAAAA");
+ equal(await buf.getCollectionHighWaterMark(), 0,
+ "High water mark should be 0 after setting sync ID");
+
+ info("Insert items and set collection last modified");
+ await buf.store([{
+ id: "menu",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ modified: 125.45,
+ }, {
+ id: "folderAAAAAA",
+ type: "folder",
+ children: [],
+ }], { needsMerge: false });
+ await buf.setCollectionLastModified(123.45);
+
+ info("Set matching sync ID");
+ await buf.ensureCurrentSyncId("syncIdAAAAAA");
+ {
+ equal(await buf.getSyncId(), "syncIdAAAAAA",
+ "Should return existing sync ID");
+ strictEqual(await buf.getCollectionHighWaterMark(), 124.45,
+ "Different sync ID should reset high water mark");
+
+ let itemRows = await buf.db.execute(`
+ SELECT guid, needsMerge FROM items
+ ORDER BY guid`);
+ let itemInfos = itemRows.map(row => ({
+ guid: row.getResultByName("guid"),
+ needsMerge: !!row.getResultByName("needsMerge"),
+ }));
+ deepEqual(itemInfos, [{
+ guid: "folderAAAAAA",
+ needsMerge: false,
+ }, {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ needsMerge: false,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ needsMerge: true,
+ }, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ needsMerge: false,
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ needsMerge: true,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ needsMerge: true,
+ }], "Matching sync ID should not reset items");
+ }
+
+ info("Set different sync ID");
+ await buf.ensureCurrentSyncId("syncIdBBBBBB");
+ {
+ equal(await buf.getSyncId(), "syncIdBBBBBB",
+ "Should replace existing sync ID");
+ strictEqual(await buf.getCollectionHighWaterMark(), 0,
+ "Different sync ID should reset high water mark");
+
+ let itemRows = await buf.db.execute(`
+ SELECT guid, needsMerge FROM items
+ ORDER BY guid`);
+ let itemInfos = itemRows.map(row => ({
+ guid: row.getResultByName("guid"),
+ needsMerge: !!row.getResultByName("needsMerge"),
+ }));
+ deepEqual(itemInfos, [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ needsMerge: true,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ needsMerge: true,
+ }, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ needsMerge: false,
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ needsMerge: true,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ needsMerge: true,
+ }], "Different sync ID should reset items");
+ }
+});
--- a/toolkit/components/places/tests/sync/xpcshell.ini
+++ b/toolkit/components/places/tests/sync/xpcshell.ini
@@ -6,12 +6,13 @@ support-files =
sync_utils_bookmarks.json
[test_bookmark_corruption.js]
[test_bookmark_deduping.js]
[test_bookmark_deletion.js]
[test_bookmark_explicit_weakupload.js]
[test_bookmark_haschanges.js]
[test_bookmark_kinds.js]
+[test_bookmark_mirror_meta.js]
[test_bookmark_structure_changes.js]
[test_bookmark_validation.js]
[test_bookmark_value_changes.js]
[test_sync_utils.js]