Bug 450290 - Sync the temp tables to the permanent tables.
authorShawn Wilsher <sdwilsh@shawnwilsher.com>
Wed, 08 Oct 2008 13:12:48 -0400
changeset 20169 20d28be6d0b0c684a2e6b546e0893f674485cbc5
parent 20168 2d4d2a1a87649407378c3947a7dd67e7015f569e
child 20170 9abd37807d16c088cbb8b178668367f925249fec
push idunknown
push userunknown
push dateunknown
bugs450290
milestone1.9.1b2pre
Bug 450290 - Sync the temp tables to the permanent tables. This changeset adds code that is run at app-startup that will flush out changes to the temporary tables into the permanent ones. For moz_places, this is done whenever we sync moz_historyvisits and when we add or modify a bookmark. For moz_historyvisits, this is done on a timer controlled by the preference places.syncDBTableIntervalInSecs. Changeset includes full test coverage for the above behaviors. r=dietrich r=Mak77
browser/installer/unix/packages-static
browser/installer/windows/packages-static
toolkit/components/places/src/Makefile.in
toolkit/components/places/src/PlacesBackground.jsm
toolkit/components/places/src/nsNavHistory.cpp
toolkit/components/places/src/nsPlacesDBFlush.js
toolkit/components/places/src/nsPlacesTriggers.h
toolkit/components/places/tests/background/head_background.js
toolkit/components/places/tests/background/test_background.js
toolkit/components/places/tests/background/test_database_sync_after_addBookmark.js
toolkit/components/places/tests/background/test_database_sync_after_addBookmark_batched.js
toolkit/components/places/tests/background/test_database_sync_after_addVisit.js
toolkit/components/places/tests/background/test_database_sync_after_addVisit_batched.js
toolkit/components/places/tests/background/test_database_sync_after_modifyBookmark.js
toolkit/components/places/tests/background/test_database_sync_after_quit_application.js
toolkit/components/places/tests/background/test_multiple_bookmarks_around_sync.js
toolkit/components/places/tests/background/test_multiple_visits_around_sync.js
--- a/browser/installer/unix/packages-static
+++ b/browser/installer/unix/packages-static
@@ -229,16 +229,17 @@ bin/components/nsSessionStore.js
 bin/components/sessionstore.xpt
 bin/components/nsURLFormatter.js
 bin/components/urlformatter.xpt
 bin/components/libbrowserdirprovider.so
 bin/components/libbrowsercomps.so
 bin/components/txEXSLTRegExFunctions.js
 bin/components/nsLivemarkService.js
 bin/components/nsTaggingService.js
+bin/components/nsPlacesDBFlush.js
 bin/components/nsDefaultCLH.js
 bin/components/nsContentPrefService.js
 bin/components/nsContentDispatchChooser.js
 bin/components/nsHandlerService.js
 bin/components/nsWebHandlerApp.js
 bin/components/libdbusservice.so
 bin/components/aboutRobots.js
 bin/components/nsBadCertHandler.js
--- a/browser/installer/windows/packages-static
+++ b/browser/installer/windows/packages-static
@@ -236,16 +236,17 @@ bin\components\nsSessionStore.js
 bin\components\sessionstore.xpt
 bin\components\nsURLFormatter.js
 bin\components\urlformatter.xpt
 bin\components\browserdirprovider.dll
 bin\components\brwsrcmp.dll
 bin\components\txEXSLTRegExFunctions.js
 bin\components\nsLivemarkService.js
 bin\components\nsTaggingService.js
+bin\components\nsPlacesDBFlush.js
 bin\components\nsDefaultCLH.js
 bin\components\nsContentPrefService.js
 bin\components\nsContentDispatchChooser.js
 bin\components\nsHandlerService.js
 bin\components\nsWebHandlerApp.js
 bin\components\aboutRobots.js
 bin\components\nsBadCertHandler.js
 
--- a/toolkit/components/places/src/Makefile.in
+++ b/toolkit/components/places/src/Makefile.in
@@ -100,16 +100,17 @@ EXTRA_DSO_LDOPTS += \
 	$(MOZ_UNICHARUTIL_LIBS) \
 	$(MOZ_COMPONENT_LIBS) \
 	$(NULL)
 
 LOCAL_INCLUDES += -I$(srcdir)/../../build
 
 EXTRA_PP_COMPONENTS = nsLivemarkService.js \
                       nsTaggingService.js \
+                      nsPlacesDBFlush.js \
                       $(NULL)
 
 EXTRA_JS_MODULES = \
   utils.js \
   PlacesBackground.jsm \
   $(NULL)
 
 EXTRA_PP_JS_MODULES = utils.js
--- a/toolkit/components/places/src/PlacesBackground.jsm
+++ b/toolkit/components/places/src/PlacesBackground.jsm
@@ -44,16 +44,17 @@ var EXPORTED_SYMBOLS = [ "PlacesBackgrou
 ////////////////////////////////////////////////////////////////////////////////
 //// Constants
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 const kQuitApplication = "quit-application";
+const kPlacesBackgroundShutdown = "places-background-shutdown";
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsPlacesBackgound class
 
 function nsPlacesBackground()
 {
   let tm = Cc["@mozilla.org/thread-manager;1"].
            getService(Ci.nsIThreadManager);
@@ -79,16 +80,22 @@ nsPlacesBackground.prototype = {
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function PlacesBackground_observe(aSubject, aTopic, aData)
   {
     if (aTopic == kQuitApplication) {
+      // Notify consumers that we are shutting down.
+      let os = Cc["@mozilla.org/observer-service;1"].
+               getService(Ci.nsIObserverService);
+      os.notifyObservers(null, kPlacesBackgroundShutdown, null);
+
+      // Now shut the thread down.
       this._thread.shutdown();
       this._thread = null;
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsISupports
 
--- a/toolkit/components/places/src/nsNavHistory.cpp
+++ b/toolkit/components/places/src/nsNavHistory.cpp
@@ -986,16 +986,19 @@ nsNavHistory::InitTempTables()
   rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "CREATE INDEX moz_places_temp_visitcount ON moz_places_temp (visit_count)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "CREATE INDEX moz_places_temp_frecencyindex ON moz_places_temp (frecency)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = mDBConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES_SYNC_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+
 
   // moz_historyvisits_temp
   rv = mDBConn->ExecuteSimpleSQL(CREATE_MOZ_HISTORYVISITS_TEMP);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "CREATE INDEX moz_historyvisits_temp_placedateindex "
     "ON moz_historyvisits_temp (place_id, visit_date)"));
@@ -1006,16 +1009,19 @@ nsNavHistory::InitTempTables()
     "ON moz_historyvisits_temp (from_visit)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "CREATE INDEX moz_historyvisits_temp_dateindex "
     "ON moz_historyvisits_temp (visit_date)"));
   NS_ENSURE_SUCCESS(rv, rv);
 
+  rv = mDBConn->ExecuteSimpleSQL(CREATE_MOZ_HISTORYVISITS_SYNC_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
 nsresult
 nsNavHistory::InitViews()
 {
   nsresult rv;
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/src/nsPlacesDBFlush.js
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+const kPlacesBackgroundShutdown = "places-background-shutdown";
+
+const kSyncPrefName = "syncDBTableIntervalInSecs";
+const kDefaultSyncInterval = 120;
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsPlacesDBFlush class
+
+function nsPlacesDBFlush()
+{
+  //////////////////////////////////////////////////////////////////////////////
+  //// Smart Getters
+
+  this.__defineGetter__("_db", function() {
+    delete this._db;
+    return this._db = Cc["@mozilla.org/browser/nav-history-service;1"].
+                      getService(Ci.nsPIPlacesDatabase).
+                      DBConnection;
+  });
+
+  this.__defineGetter__("_bh", function() {
+    delete this._bh;
+    return this._bh = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+                      getService(Ci.nsINavBookmarksService);
+  });
+
+
+  // Get our sync interval
+  this._prefs = Cc["@mozilla.org/preferences-service;1"].
+                getService(Ci.nsIPrefService).
+                getBranch("places.");
+  try {
+    // We want to silently fail if the preference does not exist, and use a
+    // default to fallback to.
+    this._syncInterval = this._prefs.getIntPref(kSyncPrefName);
+    if (this._syncInterval <= 0)
+      this._syncInterval = kDefaultSyncInterval;
+  }
+  catch (e) {
+    // The preference did not exist, so default to two minutes.
+    this._syncInterval = kDefaultSyncInterval;
+  }
+
+  // Register observers
+  this._bh.addObserver(this, false);
+
+  this._prefs.QueryInterface(Ci.nsIPrefBranch2).addObserver("", this, false);
+
+  let os = Cc["@mozilla.org/observer-service;1"].
+           getService(Ci.nsIObserverService);
+  os.addObserver(this, kPlacesBackgroundShutdown, false);
+
+  // Create our timer to update everything
+  this._timer = this._newTimer();
+}
+
+nsPlacesDBFlush.prototype = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIObserver
+
+  observe: function DBFlush_observe(aSubject, aTopic, aData)
+  {
+    if (aTopic == kPlacesBackgroundShutdown) {
+      this._bh.removeObserver(this);
+      this._timer.cancel();
+      this._timer = null;
+      this._syncAll();
+    }
+    else if (aTopic == "nsPref:changed" && aData == kSyncPrefName) {
+      // Get the new pref value, and then update our timer
+      this._syncInterval = aSubject.getIntPref(kSyncPrefName);
+      if (this._syncInterval <= 0)
+        this._syncInterval = kDefaultSyncInterval;
+
+      // We may have canceled the timer already for batch updates, so we want to
+      // exit early.
+      if (!this._timer)
+        return;
+
+      this._timer.cancel();
+      this._timer = this._newTimer();
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsINavBookmarkObserver
+
+  onBeginUpdateBatch: function DBFlush_onBeginUpdateBatch()
+  {
+    this._inBatchMode = true;
+
+    // We do not want to sync while we are doing batch work.
+    this._timer.cancel();
+    this._timer = null;
+  },
+
+  onEndUpdateBatch: function DBFlush_onEndUpdateBatch()
+  {
+    this._inBatchMode = false;
+
+    // We need to sync and restore our timer now.
+    this._syncAll();
+    this._timer = this._newTimer();
+  },
+
+  onItemAdded: function() this._syncMozPlaces(),
+
+  onItemChanged: function DBFlush_onItemChanged(aItemId, aProperty,
+                                                         aIsAnnotationProperty,
+                                                         aValue)
+  {
+    if (aProperty == "uri")
+      this._syncMozPlaces();
+  },
+
+  onItemRemoved: function() { },
+  onItemVisited: function() { },
+  onItemMoved: function() { },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsITimerCallback
+
+  notify: function() this._syncAll(),
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsPlacesDBFlush
+
+  /**
+   * Dispatches an event to the background thread to run _doSyncMozX("places").
+   */
+  _syncMozPlaces: function DBFlush_syncMozPlaces()
+  {
+    // No need to do extra work if we are in batch mode
+    if (this._inBatchMode)
+      return;
+
+    let self = this;
+    PlacesBackground.dispatch({
+      run: function() self._doSyncMozX("places")
+    }, Ci.nsIEventTarget.DISPATCH_NORMAL);
+  },
+
+  /**
+   * Dispatches an event to the background thread to sync all temporary tables.
+   */
+  _syncAll: function DBFlush_syncAll()
+  {
+    let self = this;
+    PlacesBackground.dispatch({
+      run: function() {
+        // We try to get a transaction, but if we can't don't worry
+        let ourTransaction = false;
+        try {
+          this._db.beginTransaction();
+          ourTransaction = true;
+        }
+        catch (e) { }
+
+        try {
+          // This needs to also sync moz_places in order to maintain data
+          // integrity
+          self._doSyncMozX("places");
+          self._doSyncMozX("historyvisits");
+        }
+        catch (e) {
+          if (ourTransaction)
+            this._db.rollbackTransaction();
+          throw e;
+        }
+
+        if (ourTransaction)
+          this._db.commitTransaction();
+      }
+    }, Ci.nsIEventTarget.DISPATCH_NORMAL);
+  },
+
+  /**
+   * Synchronizes the moz_{aName} and moz_{aName}_temp by copying all the data
+   * from the temporary table into the permanent one.  It then deletes the data
+   * in the temporary table.  All of this is done in a transaction that is
+   * rolled back upon failure at any point.
+   */
+  _doSyncMozX: function DBFlush_doSyncMozX(aName)
+  {
+    // Delete all the data in the temp table.
+    // We have triggers setup that ensure that the data is transfered over
+   // upon deletion.
+   this._db.executeSimpleSQL("DELETE FROM moz_" + aName + "_temp");
+  },
+
+  /**
+   * Creates a new timer bases on this._timerInterval.
+   *
+   * @returns a REPEATING_SLACK nsITimer that runs every this._timerInterval.
+   */
+  _newTimer: function DBFlush_newTimer()
+  {
+    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    timer.initWithCallback(this, this._syncInterval * 1000,
+                           Ci.nsITimer.TYPE_REPEATING_SLACK);
+    return timer;
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsISupports
+
+  classDescription: "Used to synchronize the temporary and permanent tables of Places",
+  classID: Components.ID("c1751cfc-e8f1-4ade-b0bb-f74edfb8ef6a"),
+  contractID: "@mozilla.org/places/sync;1",
+  _xpcom_categories: [{
+    category: "profile-after-change",
+  }],
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIObserver,
+    Ci.nsINavBookmarkObserver,
+    Ci.nsITimerCallback,
+  ])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// Module Registration
+
+let components = [nsPlacesDBFlush];
+function NSGetModule(compMgr, fileSpec)
+{
+  return XPCOMUtils.generateModule(components);
+}
--- a/toolkit/components/places/src/nsPlacesTriggers.h
+++ b/toolkit/components/places/src/nsPlacesTriggers.h
@@ -197,9 +197,31 @@
         "place_id = IFNULL(NEW.place_id, OLD.place_id), " \
         "visit_date = IFNULL(NEW.visit_date, OLD.visit_date), " \
         "visit_type = IFNULL(NEW.visit_type, OLD.visit_type), " \
         "session = IFNULL(NEW.session, OLD.session) " \
     "WHERE id = OLD.id; " \
   "END" \
 )
 
+/**
+ * This trigger moves the data out of a temporary table into the permanent one
+ * before deleting from the temporary table.
+ *
+ * Note - it's OK to use an INSERT OR REPLACE here because the only conflict
+ * that will happen is the primary key.  As a result, the row will be deleted,
+ * and the replacement will be inserted with the same id.
+ */
+#define CREATE_TEMP_SYNC_TRIGGER_BASE(__table) NS_LITERAL_CSTRING( \
+  "CREATE TEMPORARY TRIGGER " __table "_beforedelete_trigger " \
+  "BEFORE DELETE ON " __table "_temp FOR EACH ROW " \
+  "BEGIN " \
+    "INSERT OR REPLACE INTO " __table " " \
+    "SELECT * FROM " __table "_temp " \
+    "WHERE id = OLD.id;" \
+  "END" \
+)
+#define CREATE_MOZ_PLACES_SYNC_TRIGGER \
+  CREATE_TEMP_SYNC_TRIGGER_BASE("moz_places")
+#define CREATE_MOZ_HISTORYVISITS_SYNC_TRIGGER \
+  CREATE_TEMP_SYNC_TRIGGER_BASE("moz_historyvisits")
+
 #endif // __nsPlacesTriggers_h__
copy from toolkit/components/places/tests/unit/head_bookmarks.js
copy to toolkit/components/places/tests/background/head_background.js
--- a/toolkit/components/places/tests/unit/head_bookmarks.js
+++ b/toolkit/components/places/tests/background/head_background.js
@@ -95,8 +95,181 @@ function clearDB() {
   try {
     var file = dirSvc.get('ProfD', Ci.nsIFile);
     file.append("places.sqlite");
     if (file.exists())
       file.remove(false);
   } catch(ex) { dump("Exception: " + ex); }
 }
 clearDB();
+
+/**
+ * Dumps the rows of a table out to the console.
+ *
+ * @param aName
+ *        The name of the table or view to output.
+ */
+function dump_table(aName)
+{
+  let db = Cc["@mozilla.org/browser/nav-history-service;1"].
+           getService(Ci.nsPIPlacesDatabase).
+           DBConnection;
+  let stmt = db.createStatement("SELECT * FROM " + aName);
+
+  dump("\n*** Printing data from " + aName + ":\n");
+  let count = 0;
+  while (stmt.executeStep()) {
+    let columns = stmt.numEntries;
+
+    if (count == 0) {
+      // print the column names
+      for (let i = 0; i < columns; i++)
+        dump(stmt.getColumnName(i) + "\t");
+      dump("\n");
+    }
+
+    // print the row
+    for (let i = 0; i < columns; i++) {
+      switch (stmt.getTypeOfIndex(i)) {
+        case Ci.mozIStorageValueArray.VALUE_TYPE_NULL:
+          dump("NULL\t");
+          break;
+        case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER:
+          dump(stmt.getInt64(i) + "\t");
+          break;
+        case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT:
+          dump(stmt.getDouble(i) + "\t");
+          break;
+        case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT:
+          dump(stmt.getString(i) + "\t");
+          break;
+      }
+    }
+    dump("\n");
+
+    count++;
+  }
+  dump("*** There were a total of " + count + " rows of data.\n\n");
+
+  stmt.reset();
+  stmt.finalize();
+  stmt = null;
+}
+
+/**
+ * This dispatches the observer topic "quit-application" to clean up the
+ * background thread.
+ */
+function finish_test()
+{
+  // This next bit needs to run on the main thread
+  let tm = Cc["@mozilla.org/thread-manager;1"].
+           getService(Ci.nsIThreadManager);
+  tm.mainThread.dispatch({
+    run: function()
+    {
+      // xpcshell doesn't dispatch shutdown-application
+      let os = Cc["@mozilla.org/observer-service;1"].
+               getService(Ci.nsIObserverService);
+      os.notifyObservers(null, "quit-application", null);
+      do_test_finished();
+    }
+  }, Ci.nsIEventTarget.DISPATCH_NORMAL);
+}
+
+/**
+ * Function tests to see if the place associated with the bookmark with id
+ * aBookmarkId has the uri aExpectedURI.  The event will call finish_test() if
+ * aFinish is true.
+ *
+ * @param aBookmarkId
+ *        The bookmark to check against.
+ * @param aExpectedURI
+ *        The URI we expect to be in moz_places.
+ * @param aExpected
+ *        Indicates if we expect to get a result or not.
+ * @param [optional] aFinish
+ *        Indicates if the test should be completed or not.
+ */
+function new_test_bookmark_uri_event(aBookmarkId, aExpectedURI, aExpected,                                           aFinish)
+{
+  return {
+    run: function()
+    {
+      let db = Cc["@mozilla.org/browser/nav-history-service;1"].
+               getService(Ci.nsPIPlacesDatabase).
+               DBConnection;
+      let stmt = db.createStatement(
+        "SELECT moz_places.url " +
+        "FROM moz_bookmarks INNER JOIN moz_places " +
+        "ON moz_bookmarks.fk = moz_places.id " +
+        "WHERE moz_bookmarks.id = ?1"
+      );
+      stmt.bindInt64Parameter(0, aBookmarkId);
+
+      if (aExpected) {
+        do_check_true(stmt.executeStep());
+        do_check_eq(stmt.getUTF8String(0), aExpectedURI);
+      }
+      else {
+        do_check_false(stmt.executeStep());
+      }
+      stmt.reset();
+      stmt.finalize();
+      stmt = null;
+
+      if (aFinish)
+        finish_test();
+    }
+  };
+}
+
+/**
+ * Function tests to see if the place associated with the visit with id aVisitId
+ * has the uri aExpectedURI.  The event will call finish_test() if aFinish is
+ * true.
+ *
+ * @param aVisitId
+ *        The visit to check against.
+ * @param aExpectedURI
+ *        The URI we expect to be in moz_places.
+ * @param aExpected
+ *        Indicates if we expect to get a result or not.
+ * @param [optional] aFinish
+ *        Indicates if the test should be completed or not.
+ */
+function new_test_visit_uri_event(aVisitId, aExpectedURI, aExpected, aFinish)
+{
+  return {
+    run: function()
+    {
+      let db = Cc["@mozilla.org/browser/nav-history-service;1"].
+               getService(Ci.nsPIPlacesDatabase).
+               DBConnection;
+      let stmt = db.createStatement(
+        "SELECT moz_places.url " +
+        "FROM moz_historyvisits INNER JOIN moz_places " +
+        "ON moz_historyvisits.place_id = moz_places.id " +
+        "WHERE moz_historyvisits.id = ?1"
+      );
+      stmt.bindInt64Parameter(0, aVisitId);
+
+      if (aExpected) {
+        do_check_true(stmt.executeStep());
+        do_check_eq(stmt.getUTF8String(0), aExpectedURI);
+      }
+      else {
+        do_check_false(stmt.executeStep());
+      }
+      stmt.reset();
+      stmt.finalize();
+      stmt = null;
+
+      if (aFinish)
+        finish_test();
+    }
+  };
+}
+
+// profile-after-change doesn't create components in xpcshell, so we have to do
+// it ourselves
+Cc["@mozilla.org/places/sync;1"].getService(Ci.nsISupports);
+
--- a/toolkit/components/places/tests/background/test_background.js
+++ b/toolkit/components/places/tests/background/test_background.js
@@ -34,20 +34,16 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-
 function test_service_exists()
 {
   do_check_neq(PlacesBackground, null);
 }
 
 function test_isOnCurrentThread()
 {
   do_check_false(PlacesBackground.isOnCurrentThread());
@@ -82,20 +78,42 @@ function test_two_events_same_thread()
   Components.utils.import("resource://gre/modules/PlacesBackground.jsm", obj1);
   obj1.PlacesBackground.dispatch(event, Ci.nsIEventTarget.DISPATCH_SYNC);
   let obj2 = { };
   Components.utils.import("resource://gre/modules/PlacesBackground.jsm", obj2);
   obj2.PlacesBackground.dispatch(event, Ci.nsIEventTarget.DISPATCH_SYNC);
   do_check_eq(event.thread1, event.thread2);
 }
 
+function test_places_background_shutdown_topic()
+{
+  // Ensures that the places shutdown topic is dispatched before the thread is
+  // shutdown.
+  let os = Cc["@mozilla.org/observer-service;1"].
+           getService(Ci.nsIObserverService);
+  os.addObserver({
+    observe: function(aSubject, aTopic, aData)
+    {
+      // We should still be able to dispatch an event without throwing now!
+      PlacesBackground.dispatch({
+        run: function()
+        {
+          do_test_finished();
+        }
+      }, Ci.nsIEventTarget.DISPATCH_NORMAL);
+    }
+  }, "places-background-shutdown", false);
+  do_test_pending();
+}
+
 let tests = [
   test_service_exists,
   test_isOnCurrentThread,
   test_two_events_same_thread,
+  test_places_background_shutdown_topic,
 ];
 
 function run_test()
 {
   for (let i = 0; i < tests.length; i++)
     tests[i]();
 
   // xpcshell doesn't dispatch shutdown-application
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_database_sync_after_addBookmark.js
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+
+function run_test()
+{
+  // First insert it
+  let bh = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+           getService(Ci.nsINavBookmarksService);
+  let id = bh.insertBookmark(bh.unfiledBookmarksFolder, uri(TEST_URI),
+                             bh.DEFAULT_INDEX, "test");
+
+  PlacesBackground.dispatch(new_test_bookmark_uri_event(id, TEST_URI, true, true),
+                            Ci.nsIEventTarget.DISPATCH_NORMAL);
+  do_test_pending();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_database_sync_after_addBookmark_batched.js
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+
+function run_test()
+{
+  // First insert it
+  let bh = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+           getService(Ci.nsINavBookmarksService);
+  let id = null;
+  bh.runInBatchMode({
+    runBatched: function(aUserData)
+    {
+      id = bh.insertBookmark(bh.unfiledBookmarksFolder, uri(TEST_URI),
+                             bh.DEFAULT_INDEX, "test");
+
+      PlacesBackground.dispatch(new_test_bookmark_uri_event(id, TEST_URI, false),
+                                Ci.nsIEventTarget.DISPATCH_SYNC);
+    }
+  }, null);
+  do_check_neq(id, null);
+  PlacesBackground.dispatch(new_test_bookmark_uri_event(id, TEST_URI, true, true),
+                            Ci.nsIEventTarget.DISPATCH_NORMAL);
+  do_test_pending();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_database_sync_after_addVisit.js
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+const kSyncPrefName = "syncDBTableIntervalInSecs";
+const SYNC_INTERVAL = 1;
+
+function run_test()
+{
+  // First set the preference for the timer to a small value
+  let prefs = Cc["@mozilla.org/preferences-service;1"].
+              getService(Ci.nsIPrefService).
+              getBranch("places.");
+  prefs.setIntPref(kSyncPrefName, SYNC_INTERVAL);
+
+  // Now add the visit
+  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+           getService(Ci.nsINavHistoryService);
+  let id = hs.addVisit(uri(TEST_URI), Date.now() * 1000, null,
+                       hs.TRANSITION_TYPED, false, 0);
+
+  // Check the visit, but after enough time has passed for the DB flush service
+  // to have fired it's timer.
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({
+    notify: function(aTimer)
+    {
+      PlacesBackground.dispatch(new_test_visit_uri_event(id, TEST_URI, true, true),
+                                Ci.nsIEventTarget.DISPATCH_NORMAL);
+    }
+  }, (SYNC_INTERVAL * 1000) * 2, Ci.nsITimer.TYPE_ONE_SHOT);
+  do_test_pending();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_database_sync_after_addVisit_batched.js
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+
+function run_test()
+{
+  // Now add the visit in batch mode
+  let bh = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+           getService(Ci.nsINavBookmarksService);
+  let id = null;
+  bh.runInBatchMode({
+    runBatched: function(aUserData)
+    {
+      let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+               getService(Ci.nsINavHistoryService);
+
+      id = hs.addVisit(uri(TEST_URI), Date.now() * 1000, null,
+                       hs.TRANSITION_TYPED, false, 0);
+      PlacesBackground.dispatch(new_test_visit_uri_event(id, TEST_URI, false),
+                                Ci.nsIEventTarget.DISPATCH_SYNC);
+    }
+  }, null);
+  do_check_neq(id, null);
+  PlacesBackground.dispatch(new_test_visit_uri_event(id, TEST_URI, true, true),
+                            Ci.nsIEventTarget.DISPATCH_NORMAL);
+  do_test_pending();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_database_sync_after_modifyBookmark.js
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+const MODIFIED_URI = "http://test.com/index.html";
+
+function run_test()
+{
+  // First insert it
+  let bh = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+           getService(Ci.nsINavBookmarksService);
+  let id = bh.insertBookmark(bh.unfiledBookmarksFolder, uri(TEST_URI),
+                             bh.DEFAULT_INDEX, "test");
+
+  // Dispatch the check synchronously so we don't modify the data before this
+  // test runs
+  PlacesBackground.dispatch(new_test_bookmark_uri_event(id, TEST_URI, true),
+                            Ci.nsIEventTarget.DISPATCH_SYNC);
+
+  // Now modify the bookmark
+  bh.changeBookmarkURI(id, uri(MODIFIED_URI));
+
+  PlacesBackground.dispatch(new_test_bookmark_uri_event(id, MODIFIED_URI, true, true),
+                            Ci.nsIEventTarget.DISPATCH_NORMAL);
+  do_test_pending();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_database_sync_after_quit_application.js
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+const kSyncPrefName = "syncDBTableIntervalInSecs";
+const SYNC_INTERVAL = 600; // ten minutes
+
+function run_test()
+{
+  // First set the preference for the timer to a really large value so it won't
+  // run before the test finishes.
+  let prefs = Cc["@mozilla.org/preferences-service;1"].
+              getService(Ci.nsIPrefService).
+              getBranch("places.");
+  prefs.setIntPref(kSyncPrefName, SYNC_INTERVAL);
+
+  // Now add the visit
+  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+           getService(Ci.nsINavHistoryService);
+  let id = hs.addVisit(uri(TEST_URI), Date.now() * 1000, null,
+                       hs.TRANSITION_TYPED, false, 0);
+
+  // Notify that we are quitting the app - we should sync!
+  let os = Cc["@mozilla.org/observer-service;1"].
+           getService(Ci.nsIObserverService);
+  os.notifyObservers(null, "quit-application", null);
+
+  // Check the visit.  The background thread should have joined with the main
+  // thread by now if everything is working correctly.
+  new_test_visit_uri_event(id, TEST_URI, true, false).run();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_multiple_bookmarks_around_sync.js
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This test ensures that adding a bookmark (which has an implicit sync), then
+ * adding another one that has the same place, we end up with only one entry in
+ * moz_places.
+ */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+
+let db = Cc["@mozilla.org/browser/nav-history-service;1"].
+         getService(Ci.nsPIPlacesDatabase).
+         DBConnection;
+
+function run_test()
+{
+  // Add the first bookmark
+  let bh = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+           getService(Ci.nsINavBookmarksService);
+  let id1 = bh.insertBookmark(bh.unfiledBookmarksFolder, uri(TEST_URI),
+                              bh.DEFAULT_INDEX, "test");
+
+  // Ensure it was added
+  PlacesBackground.dispatch(new_test_bookmark_uri_event(id1, TEST_URI, true),
+                            Ci.nsIEventTarget.DISPATCH_SYNC);
+
+  // Get the place_id
+  let stmt = db.createStatement(
+    "SELECT fk " +
+    "FROM moz_bookmarks " +
+    "WHERE id = ?"
+  );
+  stmt.bindInt64Parameter(0, id1);
+  do_check_true(stmt.executeStep());
+  let place_id = stmt.getInt64(0);
+  stmt.finalize();
+  stmt = null;
+
+  // Now we add another bookmark to a different folder
+  let id2 = bh.insertBookmark(bh.toolbarFolder, uri(TEST_URI),
+                              bh.DEFAULT_INDEX, "test");
+  do_check_neq(id1, id2);
+
+  // Ensure it was added
+  PlacesBackground.dispatch(new_test_bookmark_uri_event(id2, TEST_URI, true),
+                            Ci.nsIEventTarget.DISPATCH_SYNC);
+
+  // Check to make sure we have the same place_id
+  stmt = db.createStatement(
+    "SELECT * " +
+    "FROM moz_bookmarks " +
+    "WHERE id = ?1 " +
+    "AND fk = ?2"
+  );
+  stmt.bindInt64Parameter(0, id2);
+  stmt.bindInt64Parameter(1, place_id);
+  do_check_true(stmt.executeStep());
+  stmt.finalize();
+  stmt = null;
+
+  // finish_test() calls do_test_finished, so we call do_test_pending()...
+  do_test_pending();
+  finish_test();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/background/test_multiple_visits_around_sync.js
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is mozilla.org code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This test ensures that when adding a visit, then syncing, and adding another
+ * visit to the same url creates two visits and that we only end up with one
+ * entry in moz_places.
+ */
+
+Components.utils.import("resource://gre/modules/PlacesBackground.jsm");
+
+const TEST_URI = "http://test.com/";
+const kSyncPrefName = "syncDBTableIntervalInSecs";
+const SYNC_INTERVAL = 1;
+
+function run_test()
+{
+  // First set the preference for the timer to a small value
+  let prefs = Cc["@mozilla.org/preferences-service;1"].
+              getService(Ci.nsIPrefService).
+              getBranch("places.");
+  prefs.setIntPref(kSyncPrefName, SYNC_INTERVAL);
+
+  // Now add the first visit
+  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+           getService(Ci.nsINavHistoryService);
+  let id = hs.addVisit(uri(TEST_URI), Date.now() * 1000, null,
+                       hs.TRANSITION_TYPED, false, 0);
+
+  // Check the visit, but after enough time has passed for the DB flush service
+  // to have fired it's timer.
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({
+    notify: function(aTimer)
+    {
+      PlacesBackground.dispatch(new_test_visit_uri_event(id, TEST_URI, true),
+                                Ci.nsIEventTarget.DISPATCH_SYNC);
+
+      // Get the place_id and pass it on
+      let db = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+      let stmt = db.createStatement(
+        "SELECT place_id " +
+        "FROM moz_historyvisits " +
+        "WHERE id = ?"
+      );
+      stmt.bindInt64Parameter(0, id);
+      do_check_true(stmt.executeStep());
+      continue_test(id, stmt.getInt64(0));
+      stmt.finalize();
+      stmt = null;
+    }
+  }, (SYNC_INTERVAL * 1000) * 2, Ci.nsITimer.TYPE_ONE_SHOT);
+  do_test_pending();
+}
+
+function continue_test(aLastVisitId, aPlaceId)
+{
+  // Now we add another visit
+  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+           getService(Ci.nsINavHistoryService);
+  let id = hs.addVisit(uri(TEST_URI), Date.now() * 1000, null,
+                       hs.TRANSITION_TYPED, false, 0);
+  do_check_neq(aLastVisitId, id);
+
+  // Check the visit, but after enough time has passed for the DB flush service
+  // to have fired it's timer.
+  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  timer.initWithCallback({
+    notify: function(aTimer)
+    {
+      PlacesBackground.dispatch(new_test_visit_uri_event(id, TEST_URI, true),
+                                Ci.nsIEventTarget.DISPATCH_SYNC);
+
+      // Check to make sure we have the same place_id
+      let db = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+      let stmt = db.createStatement(
+        "SELECT * " +
+        "FROM moz_historyvisits " +
+        "WHERE id = ?1 " +
+        "AND place_id = ?2"
+      );
+      stmt.bindInt64Parameter(0, id);
+      stmt.bindInt64Parameter(1, aPlaceId);
+      do_check_true(stmt.executeStep());
+      stmt.finalize();
+      stmt = null;
+
+      finish_test();
+    }
+  }, (SYNC_INTERVAL * 1000) * 2, Ci.nsITimer.TYPE_ONE_SHOT);
+}