Bug 699751 - Change schema for new inline autocomplete.
authorMichael Ventnor<ventnor.bugzilla@gmail.com>
Thu, 19 Jan 2012 12:31:23 +0100
changeset 87329 3898e1c52fa332cfb80c449bb4e2be48229da8d1
parent 87328 f23efef7f6160a1620ee5d3af2bc2596f31079a7
child 87330 953bde82b7a7d2ea4408b1792791612ac8434214
push idunknown
push userunknown
push dateunknown
bugs699751
milestone12.0a1
Bug 699751 - Change schema for new inline autocomplete. Original patch by Michael Ventnor, further improved by David Dahl <ddahl@mozilla.com>. r=mak
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/SQLFunctions.cpp
toolkit/components/places/SQLFunctions.h
toolkit/components/places/nsPlacesIndexes.h
toolkit/components/places/nsPlacesTables.h
toolkit/components/places/nsPlacesTriggers.h
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/test_current_from_v10.js
toolkit/components/places/tests/unit/test_hosts_triggers.js
toolkit/components/places/tests/unit/xpcshell.ini
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -732,16 +732,23 @@ Database::InitSchema(bool* aDatabaseMigr
 
       if (currentSchemaVersion < 16) {
         rv = MigrateV16Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       // Firefox 11 uses schema version 16.
 
+      if (currentSchemaVersion < 17) {
+        rv = MigrateV17Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 12 uses schema version 17.
+
       // Schema Upgrades must add migration code here.
 
       rv = UpdateBookmarkRootTitles();
       // We don't want a broken localization to cause us to think
       // the database is corrupt and needs to be replaced.
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
@@ -775,16 +782,22 @@ Database::InitSchema(bool* aDatabaseMigr
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_VISITDATE);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // moz_inputhistory.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_INPUTHISTORY);
     NS_ENSURE_SUCCESS(rv, rv);
 
+    // moz_hosts.
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HOSTS_FRECENCYHOST);
+    NS_ENSURE_SUCCESS(rv, rv);
+
     // moz_bookmarks.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACETYPE);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PARENTPOSITION);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACELASTMODIFIED);
@@ -922,30 +935,40 @@ Database::InitFunctions()
   nsresult rv = GetUnreversedHostFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = MatchAutoCompleteFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = CalculateFrecencyFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = GenerateGUIDFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
+  rv = FixupURLFunction::create(mMainConn);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 nsresult
 Database::InitTempTriggers()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsresult rv = mMainConn->ExecuteSimpleSQL(CREATE_HISTORYVISITS_AFTERINSERT_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = mMainConn->ExecuteSimpleSQL(CREATE_HISTORYVISITS_AFTERDELETE_TRIGGER);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // Add the triggers that update the moz_hosts table as necessary.
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERINSERT_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERDELETE_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TRIGGER);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
 nsresult
 Database::UpdateBookmarkRootTitles()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -1622,16 +1645,66 @@ Database::MigrateV16Up()
     "SET guid = GENERATE_GUID() "
     "WHERE guid ISNULL "
   ));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+nsresult
+Database::MigrateV17Up()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  bool tableExists = false;
+
+  nsresult rv = mMainConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &tableExists);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!tableExists) {
+    // For anyone who used in-development versions of this autocomplete,
+    // drop the old tables and its indexes.
+    rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "DROP INDEX IF EXISTS moz_hostnames_frecencyindex"
+    ));
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "DROP TABLE IF EXISTS moz_hostnames"
+    ));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Add the moz_hosts table so we can get hostnames for URL autocomplete.
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HOSTS_FRECENCYHOST);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // Fill the moz_hosts table with all the domains in moz_places.
+  nsCOMPtr<mozIStorageAsyncStatement> fillHostsStmt;
+  rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING(
+    "INSERT OR IGNORE INTO moz_hosts (host, frecency) "
+        "SELECT fixup_url(get_unreversed_host(h.rev_host)) AS host, "
+               "(SELECT MAX(frecency) FROM moz_places "
+                "WHERE rev_host = h.rev_host OR rev_host = h.rev_host || 'www.'"
+               ") AS frecency "
+        "FROM moz_places h "
+        "WHERE LENGTH(h.rev_host) > 1 "
+        "GROUP BY h.rev_host"
+  ), getter_AddRefs(fillHostsStmt));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCOMPtr<mozIStoragePendingStatement> ps;
+  rv = fillHostsStmt->ExecuteAsync(nsnull, getter_AddRefs(ps));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 void
 Database::Shutdown()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mShuttingDown);
 
   mMainThreadStatements.FinalizeStatements();
   mMainThreadAsyncStatements.FinalizeStatements();
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -42,17 +42,17 @@
 #include "nsWeakReference.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIObserver.h"
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 16
+#define DATABASE_SCHEMA_VERSION 17
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // Fired when initialization fails due to a locked database.
 #define TOPIC_DATABASE_LOCKED "places-database-locked"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
@@ -294,16 +294,17 @@ protected:
   nsresult MigrateV8Up();
   nsresult MigrateV9Up();
   nsresult MigrateV10Up();
   nsresult MigrateV11Up();
   nsresult MigrateV13Up();
   nsresult MigrateV14Up();
   nsresult MigrateV15Up();
   nsresult MigrateV16Up();
+  nsresult MigrateV17Up();
 
   nsresult UpdateBookmarkRootTitles();
   nsresult CheckAndUpdateGUIDs();
 
 private:
   ~Database();
 
   /**
--- a/toolkit/components/places/SQLFunctions.cpp
+++ b/toolkit/components/places/SQLFunctions.cpp
@@ -731,10 +731,61 @@ namespace places {
     }
     else {
       result->SetAsAString(EmptyString());
     }
     NS_ADDREF(*_result = result);
     return NS_OK;
   }
 
+////////////////////////////////////////////////////////////////////////////////
+//// Fixup URL Function
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// FixupURLFunction
+
+  /* static */
+  nsresult
+  FixupURLFunction::create(mozIStorageConnection *aDBConn)
+  {
+    nsRefPtr<FixupURLFunction> function = new FixupURLFunction();
+    nsresult rv = aDBConn->CreateFunction(
+      NS_LITERAL_CSTRING("fixup_url"), 1, function
+    );
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  NS_IMPL_THREADSAFE_ISUPPORTS1(
+    FixupURLFunction,
+    mozIStorageFunction
+  )
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// mozIStorageFunction
+
+  NS_IMETHODIMP
+  FixupURLFunction::OnFunctionCall(mozIStorageValueArray *aArguments,
+                                   nsIVariant **_result)
+  {
+    // Must have non-null function arguments.
+    MOZ_ASSERT(aArguments);
+
+    nsAutoString src;
+    aArguments->GetString(0, src);
+
+    nsCOMPtr<nsIWritableVariant> result =
+      do_CreateInstance("@mozilla.org/variant;1");
+    NS_ENSURE_STATE(result);
+
+    // Remove common URL hostname prefixes
+    if (StringBeginsWith(src, NS_LITERAL_STRING("www."))) {
+      src.Cut(0, 4);
+    }
+
+    result->SetAsAString(src);
+    NS_ADDREF(*_result = result);
+    return NS_OK;
+  }
+
 } // namespace places
 } // namespace mozilla
--- a/toolkit/components/places/SQLFunctions.h
+++ b/toolkit/components/places/SQLFunctions.h
@@ -265,12 +265,40 @@ public:
    * Registers the function with the specified database connection.
    *
    * @param aDBConn
    *        The database connection to register with.
    */
   static nsresult create(mozIStorageConnection *aDBConn);
 };
 
+
+////////////////////////////////////////////////////////////////////////////////
+//// Fixup URL Function
+
+/**
+ * Make a given URL more suitable for searches, by removing common prefixes
+ * such as "www."
+ *
+ * @param url
+ *        A URL.
+ * @return
+ *        The same URL, with redundant parts removed.
+ */
+class FixupURLFunction : public mozIStorageFunction
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_MOZISTORAGEFUNCTION
+
+  /**
+   * Registers the function with the specified database connection.
+   *
+   * @param aDBConn
+   *        The database connection to register with.
+   */
+  static nsresult create(mozIStorageConnection *aDBConn);
+};
+
 } // namespace places
 } // namespace storage
 
 #endif // mozilla_places_SQLFunctions_h_
--- a/toolkit/components/places/nsPlacesIndexes.h
+++ b/toolkit/components/places/nsPlacesIndexes.h
@@ -132,16 +132,25 @@
  */
 
 #define CREATE_IDX_MOZ_ANNOS_PLACEATTRIBUTE \
   CREATE_PLACES_IDX( \
     "placeattributeindex", "moz_annos", "place_id, anno_attribute_id", "UNIQUE" \
   )
 
 /**
+ * moz_hosts
+ */
+
+#define CREATE_IDX_MOZ_HOSTS_FRECENCYHOST \
+  CREATE_PLACES_IDX( \
+    "frecencyhostindex", "moz_hosts", "frecency, host", "" \
+  )
+
+/**
  * moz_items_annos
  */
 
 #define CREATE_IDX_MOZ_ITEMSANNOS_PLACEATTRIBUTE \
   CREATE_PLACES_IDX( \
     "itemattributeindex", "moz_items_annos", "item_id, anno_attribute_id", "UNIQUE" \
   )
 
--- a/toolkit/components/places/nsPlacesTables.h
+++ b/toolkit/components/places/nsPlacesTables.h
@@ -157,16 +157,24 @@
 
 #define CREATE_MOZ_KEYWORDS NS_LITERAL_CSTRING( \
   "CREATE TABLE moz_keywords (" \
     "  id INTEGER PRIMARY KEY AUTOINCREMENT" \
     ", keyword TEXT UNIQUE" \
   ")" \
 )
 
+#define CREATE_MOZ_HOSTS NS_LITERAL_CSTRING( \
+  "CREATE TABLE moz_hosts (" \
+    "  id INTEGER PRIMARY KEY" \
+    ", host TEXT NOT NULL UNIQUE" \
+    ", frecency INTEGER" \
+  ")" \
+)
+
 // Note: this should be kept up-to-date with the definition in
 //       nsPlacesAutoComplete.js.
 #define CREATE_MOZ_OPENPAGES_TEMP NS_LITERAL_CSTRING( \
   "CREATE TEMP TABLE moz_openpages_temp (" \
     "  url TEXT PRIMARY KEY" \
     ", open_count INTEGER" \
   ")" \
 )
--- a/toolkit/components/places/nsPlacesTriggers.h
+++ b/toolkit/components/places/nsPlacesTriggers.h
@@ -75,16 +75,54 @@
       "last_visit_date = (SELECT visit_date FROM moz_historyvisits " \
                          "WHERE place_id = OLD.place_id " \
                          "ORDER BY visit_date DESC LIMIT 1) " \
     "WHERE id = OLD.place_id;" \
   "END" \
 )
 
 /**
+ * These triggers update the hostnames table whenever moz_places changes.
+ */
+#define CREATE_PLACES_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMP TRIGGER moz_places_afterinsert_trigger " \
+  "AFTER INSERT ON moz_places FOR EACH ROW " \
+  "WHEN LENGTH(NEW.rev_host) > 1 " \
+  "BEGIN " \
+    "INSERT OR REPLACE INTO moz_hosts (id, host, frecency) " \
+    "VALUES (" \
+      "(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
+      "fixup_url(get_unreversed_host(NEW.rev_host)), " \
+      "MAX((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), NEW.frecency) " \
+    "); " \
+  "END" \
+)
+
+#define CREATE_PLACES_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMP TRIGGER moz_places_afterdelete_trigger " \
+  "AFTER DELETE ON moz_places FOR EACH ROW " \
+  "BEGIN " \
+    "DELETE FROM moz_hosts " \
+    "WHERE host = fixup_url(get_unreversed_host(OLD.rev_host)) " \
+      "AND NOT EXISTS(SELECT 1 FROM moz_places WHERE rev_host = OLD.rev_host); " \
+  "END" \
+)
+
+#define CREATE_PLACES_AFTERUPDATE_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TEMP TRIGGER moz_places_afterupdate_frecency_trigger " \
+  "AFTER UPDATE OF frecency ON moz_places FOR EACH ROW " \
+  "WHEN NEW.frecency >= 0 " \
+  "BEGIN " \
+    "UPDATE moz_hosts " \
+    "SET frecency = (SELECT MAX(frecency) FROM moz_places WHERE rev_host = NEW.rev_host OR rev_host = NEW.rev_host || 'www.') " \
+    "WHERE host = fixup_url(get_unreversed_host(NEW.rev_host)); " \
+  "END" \
+)
+
+/**
  * This trigger removes a row from moz_openpages_temp when open_count reaches 0.
  *
  * @note this should be kept up-to-date with the definition in
  *       nsPlacesAutoComplete.js
  */
 #define CREATE_REMOVEOPENPAGE_CLEANUP_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger " \
   "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW " \
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -30,17 +30,17 @@
  * 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 ***** */
 
-const CURRENT_SCHEMA_VERSION = 16;
+const CURRENT_SCHEMA_VERSION = 17;
 
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
 const NS_APP_BOOKMARKS_50_FILE = "BMarks";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
--- a/toolkit/components/places/tests/migration/test_current_from_v10.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v10.js
@@ -273,16 +273,47 @@ function test_place_guid_annotation_remo
   stmt.params.attr_name = kGuidAnnotationName;
   do_check_true(stmt.executeStep());
   do_check_eq(stmt.getInt32(0), 0);
   stmt.finalize();
 
   run_next_test();
 }
 
+function test_moz_hosts()
+{
+  // This will throw if the column does not exist
+  let stmt = DBConn().createStatement(
+    "SELECT host, frecency "
+  + "FROM moz_hosts "
+  );
+  stmt.finalize();
+
+  // check the number of entries in moz_hosts equals the number of
+  // unique rev_host in moz_places
+  var query = "SELECT ("
+              + "SELECT COUNT(host) "
+              + "FROM moz_hosts), ("
+              + "SELECT COUNT(DISTINCT rev_host) "
+              + "FROM moz_places "
+              + "WHERE LENGTH(rev_host) > 1)";
+
+  stmt = DBConn().createStatement(query);
+  try {
+    stmt.executeStep();
+    let mozPlacesCount = stmt.getInt32(0);
+    let mozHostsCount = stmt.getInt32(1);
+    do_check_eq(mozPlacesCount, mozHostsCount);
+  }
+  finally {
+    stmt.finalize();
+    run_next_test();
+  }
+}
+
 function test_final_state()
 {
   // We open a new database mostly so that we can check that the settings were
   // actually saved.
   let dbFile = gProfD.clone();
   dbFile.append(kDBName);
   let db = Services.storage.openUnsharedDatabase(dbFile);
 
@@ -292,16 +323,18 @@ function test_final_state()
     do_check_eq(stmt.getString(0).toLowerCase(), "wal");
     stmt.finalize();
   }
 
   do_check_true(db.indexExists("moz_bookmarks_guid_uniqueindex"));
   do_check_true(db.indexExists("moz_places_guid_uniqueindex"));
   do_check_true(db.indexExists("moz_favicons_guid_uniqueindex"));
 
+  do_check_true(db.indexExists("moz_hosts_frecencyhostindex"));
+
   do_check_eq(db.schemaVersion, CURRENT_SCHEMA_VERSION);
 
   db.close();
   run_next_test();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Test Runner
@@ -311,16 +344,17 @@ function test_final_state()
   test_moz_bookmarks_guid_exists,
   test_bookmark_guids_non_null,
   test_bookmark_guid_annotation_imported,
   test_bookmark_guid_annotation_removed,
   test_moz_places_guid_exists,
   test_place_guids_non_null,
   test_place_guid_annotation_imported,
   test_place_guid_annotation_removed,
+  test_moz_hosts,
   test_final_state,
 ].forEach(add_test);
 
 function run_test()
 {
   setPlacesDatabase("places_v10.sqlite");
   run_next_test();
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_hosts_triggers.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the validity of various triggers that add remove hosts from moz_hosts
+ */
+
+// add some visits and remove them, add a bookmark,
+// change its uri, then remove it, and
+// for each change check that moz_hosts has correctly been updated.
+
+function isHostInMozPlaces(aURI)
+{
+  let stmt = DBConn().createStatement(
+    "SELECT url "
+    + "FROM moz_places "
+    + "WHERE url = :host"
+  );
+  let result = false;
+  stmt.params.host = aURI.spec;
+  while(stmt.executeStep()) {
+    if (stmt.row.url == aURI.spec) {
+      result = true;
+      break;
+    }
+  }
+  stmt.finalize();
+  return result;
+}
+
+function isHostInMozHosts(aURI)
+{
+  let stmt = DBConn().createStatement(
+    "SELECT host "
+    + "FROM moz_hosts "
+    + "WHERE host = :host"
+  );
+  let result = false;
+  stmt.params.host = aURI.host;
+  while(stmt.executeStep()) {
+    if (stmt.row.host == aURI.host) {
+      result = true;
+      break;
+    }
+  }
+  stmt.finalize();
+  return result;
+}
+
+let urls = [{uri: NetUtil.newURI("http://visit1.mozilla.org"),
+             expected: "visit1.mozilla.org",
+            },
+            {uri: NetUtil.newURI("http://visit2.mozilla.org"),
+             expected: "visit2.mozilla.org",
+            },
+            {uri: NetUtil.newURI("http://www.foo.mozilla.org"),
+             expected: "foo.mozilla.org",
+            },
+           ];
+
+function VisitInfo(aTransitionType, aVisitTime)
+{
+  this.transitionType =
+    aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
+  this.visitDate = aVisitTime || Date.now() * 1000;
+}
+
+const NEW_URL = "http://different.mozilla.org/";
+
+function test_moz_hosts_update()
+{
+  let places = [];
+  urls.forEach(function(url) {
+    let place = {
+                  uri: url.uri,
+                  title: "test for " + url.url,
+                  visits: [
+                    new VisitInfo(),
+                  ],
+    };
+    places.push(place);
+  });
+
+  XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
+                                     "@mozilla.org/browser/history;1",
+                                     "mozIAsyncHistory");
+
+  gHistory.updatePlaces(places, {
+    handleResult: function () {
+    },
+    handleError: function () {
+      do_throw("gHistory.updatePlaces() failed");
+    },
+    handleCompletion: function () {
+      do_check_true(isHostInMozHosts(urls[0].uri));
+      do_check_true(isHostInMozHosts(urls[1].uri));
+      // strip the WWW from the url before testing...
+      do_check_true(isHostInMozHosts(NetUtil.newURI("http://foo.mozilla.org")));
+      run_next_test();
+    }
+  });
+}
+
+function test_remove_places()
+{
+  for (let idx in urls) {
+    PlacesUtils.history.removePage(urls[idx].uri);
+  }
+
+  waitForClearHistory(function (){
+    for (let idx in urls) {
+      do_check_false(isHostInMozHosts(urls[idx].uri));
+    }
+    run_next_test();
+  });
+}
+
+function test_bookmark_changes()
+{
+  let testUri = NetUtil.newURI("http://test.mozilla.org");
+
+  let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                                     testUri,
+                                                     PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                                     "bookmark title");
+
+  do_check_true(isHostInMozPlaces(testUri));
+
+  // Change the hostname
+  PlacesUtils.bookmarks.changeBookmarkURI(itemId, NetUtil.newURI(NEW_URL));
+
+  waitForClearHistory(function (){
+    let newUri = NetUtil.newURI(NEW_URL);
+    do_check_true(isHostInMozPlaces(newUri));
+    do_check_true(isHostInMozHosts(newUri));
+    do_check_false(isHostInMozHosts(NetUtil.newURI("http://test.mozilla.org")));
+    run_next_test();
+  });
+}
+
+function test_bookmark_removal()
+{
+  let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId,
+                                                    PlacesUtils.bookmarks.DEFAULT_INDEX);
+  let newUri = NetUtil.newURI(NEW_URL);
+  PlacesUtils.bookmarks.removeItem(itemId);
+  waitForClearHistory(function (){
+    do_check_false(isHostInMozHosts(newUri));
+    run_next_test();
+  });
+
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test Runner
+
+[
+  test_moz_hosts_update,
+  test_remove_places,
+  test_bookmark_changes,
+  test_bookmark_removal,
+].forEach(add_test);
+
+function run_test()
+{
+  run_next_test();
+}
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -84,16 +84,17 @@ skip-if = os == "android"
 [test_history_autocomplete_tags.js]
 [test_history_catobs.js]
 [test_history_notifications.js]
 [test_history_observer.js]
 [test_history_removeAllPages.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_history_sidebar.js]
+[test_hosts_triggers.js]
 [test_isURIVisited.js]
 [test_isvisited.js]
 [test_lastModified.js]
 [test_livemarkService_getLivemarkIdForFeedURI.js]
 [test_markpageas.js]
 [test_moz-anno_favicon_mime_type.js]
 [test_multi_queries.js]
 # Bug 676989: test fails consistently on Android