Backed out 9 changesets (bug 888784) for failing mochitest-bc browser_426329.js on Linux and on Android chrome's test_ext_browsingData_formdata.html and robocop's testFormHistory. r=backout on a CLOSED TREE
authorSebastian Hengst <archaeopteryx@coole-files.de>
Tue, 09 Jan 2018 20:18:55 +0200
changeset 450202 9fc7e71752fd1e9764e900eec7c0b7de43d5f8db
parent 450201 c809b916352b3d4056610be28e89dc048d363493
child 450203 7a93234cb5eb9172321aea3cc0f0725d45d7ee04
push id8527
push userCallek@gmail.com
push dateThu, 11 Jan 2018 21:05:50 +0000
treeherdermozilla-beta@95342d212a7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs888784
milestone59.0a1
backs out98722ab8c2f6e9dd23dfb18e9a58d576328da8e8
d92599272745f5dd442cb1e4d66e3c18b56e42df
67c58cb32ac9cb44bee2d15be016d9065172d964
46fb8f82f2bffe2bbd27ee8bcec815e3f59d7697
73ad820d09ecafbfab3e3d2979f64677009b3ed6
18d185fa362e6a3018560cb414df06e0731b9558
10c472d10264e8fbe90f9bdc8c8706d64f75c938
51fb50c1ea68400276c4eb715c0f76e2290c0e07
80a207ed79ecdfde31d6ea1c58fd576df8146c13
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 9 changesets (bug 888784) for failing mochitest-bc browser_426329.js on Linux and on Android chrome's test_ext_browsingData_formdata.html and robocop's testFormHistory. r=backout on a CLOSED TREE Backed out changeset 98722ab8c2f6 (bug 888784) Backed out changeset d92599272745 (bug 888784) Backed out changeset 67c58cb32ac9 (bug 888784) Backed out changeset 46fb8f82f2bf (bug 888784) Backed out changeset 73ad820d09ec (bug 888784) Backed out changeset 18d185fa362e (bug 888784) Backed out changeset 10c472d10264 (bug 888784) Backed out changeset 51fb50c1ea68 (bug 888784) Backed out changeset 80a207ed79ec (bug 888784)
toolkit/components/satchel/FormHistory.jsm
toolkit/components/satchel/FormHistoryStartup.js
toolkit/components/satchel/test/unit/head_satchel.js
toolkit/components/satchel/test/unit/test_async_expire.js
toolkit/components/satchel/test/unit/test_db_update_v4.js
toolkit/components/satchel/test/unit/test_db_update_v4b.js
toolkit/components/satchel/test/unit/test_db_update_v999a.js
toolkit/components/satchel/test/unit/test_db_update_v999b.js
toolkit/components/satchel/test/unit/test_history_api.js
--- a/toolkit/components/satchel/FormHistory.jsm
+++ b/toolkit/components/satchel/FormHistory.jsm
@@ -90,29 +90,21 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidService",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
-XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
-                                  "resource://gre/modules/AsyncShutdown.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS",
-                                  "resource://gre/modules/osfile.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
-                                  "resource://gre/modules/Sqlite.jsm");
-
 
 const DB_SCHEMA_VERSION = 4;
 const DAY_IN_MS  = 86400000; // 1 day in milliseconds
 const MAX_SEARCH_TOKENS = 10;
 const NOOP = function noop() {};
-const DB_FILENAME = "formhistory.sqlite";
 
 var supportsDeletedTable = AppConstants.platform == "android";
 
 var Prefs = {
   initialized: false,
 
   get debug() { this.ensureInitialized(); return this._debug; },
   get enabled() { this.ensureInitialized(); return this._enabled; },
@@ -256,16 +248,117 @@ function makeQueryPredicates(aQueryData,
         return "lastUsed <= :" + field;
       }
     }
 
     return field + " = :" + field;
   }).join(delimiter);
 }
 
+/**
+ * Storage statement creation and parameter binding
+ */
+
+function makeCountStatement(aSearchData) {
+  let query = "SELECT COUNT(*) AS numEntries FROM moz_formhistory";
+  let queryTerms = makeQueryPredicates(aSearchData);
+  if (queryTerms) {
+    query += " WHERE " + queryTerms;
+  }
+  return dbCreateAsyncStatement(query, aSearchData);
+}
+
+function makeSearchStatement(aSearchData, aSelectTerms) {
+  let query = "SELECT " + aSelectTerms.join(", ") + " FROM moz_formhistory";
+  let queryTerms = makeQueryPredicates(aSearchData);
+  if (queryTerms) {
+    query += " WHERE " + queryTerms;
+  }
+
+  return dbCreateAsyncStatement(query, aSearchData);
+}
+
+function makeAddStatement(aNewData, aNow, aBindingArrays) {
+  let query = "INSERT INTO moz_formhistory " +
+              "(fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
+              "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
+
+  aNewData.timesUsed = aNewData.timesUsed || 1;
+  aNewData.firstUsed = aNewData.firstUsed || aNow;
+  aNewData.lastUsed = aNewData.lastUsed || aNow;
+  return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
+}
+
+function makeBumpStatement(aGuid, aNow, aBindingArrays) {
+  let query = "UPDATE moz_formhistory " +
+              "SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE guid = :guid";
+  let queryParams = {
+    lastUsed: aNow,
+    guid: aGuid,
+  };
+
+  return dbCreateAsyncStatement(query, queryParams, aBindingArrays);
+}
+
+function makeRemoveStatement(aSearchData, aBindingArrays) {
+  let query = "DELETE FROM moz_formhistory";
+  let queryTerms = makeQueryPredicates(aSearchData);
+
+  if (queryTerms) {
+    log("removeEntries");
+    query += " WHERE " + queryTerms;
+  } else {
+    log("removeAllEntries");
+    // Not specifying any fields means we should remove all entries. We
+    // won't need to modify the query in this case.
+  }
+
+  return dbCreateAsyncStatement(query, aSearchData, aBindingArrays);
+}
+
+function makeUpdateStatement(aGuid, aNewData, aBindingArrays) {
+  let query = "UPDATE moz_formhistory SET ";
+  let queryTerms = makeQueryPredicates(aNewData, ", ");
+
+  if (!queryTerms) {
+    throw Components.Exception("Update query must define fields to modify.",
+                               Cr.NS_ERROR_ILLEGAL_VALUE);
+  }
+
+  query += queryTerms + " WHERE guid = :existing_guid";
+  aNewData.existing_guid = aGuid;
+
+  return dbCreateAsyncStatement(query, aNewData, aBindingArrays);
+}
+
+function makeMoveToDeletedStatement(aGuid, aNow, aData, aBindingArrays) {
+  if (supportsDeletedTable) {
+    let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted)";
+    let queryTerms = makeQueryPredicates(aData);
+
+    if (aGuid) {
+      query += " VALUES (:guid, :timeDeleted)";
+    } else {
+      // TODO: Add these items to the deleted items table once we've sorted
+      //       out the issues from bug 756701
+      if (!queryTerms) {
+        return undefined;
+      }
+
+      query += " SELECT guid, :timeDeleted FROM moz_formhistory WHERE " + queryTerms;
+    }
+
+    aData.timeDeleted = aNow;
+
+    return dbCreateAsyncStatement(query, aData, aBindingArrays);
+  }
+
+  return null;
+}
+
 function generateGUID() {
   // string like: "{f60d9eac-9421-4abc-8491-8e8322b063d4}"
   let uuid = uuidService.generateUUID().toString();
   let raw = ""; // A string with the low bytes set to random values
   let bytes = 0;
   for (let i = 1; bytes < 12; i += 2) {
     // Skip dashes
     if (uuid[i] == "-") {
@@ -273,230 +366,392 @@ function generateGUID() {
     }
     let hexVal = parseInt(uuid[i] + uuid[i + 1], 16);
     raw += String.fromCharCode(hexVal);
     bytes++;
   }
   return btoa(raw);
 }
 
+/**
+ * Database creation and access
+ */
+
+var _dbConnection = null;
+XPCOMUtils.defineLazyGetter(this, "dbConnection", function() {
+  let dbFile;
+
+  try {
+    dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
+    dbFile.append("formhistory.sqlite");
+    log("Opening database at " + dbFile.path);
+
+    _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+    dbInit();
+  } catch (e) {
+    if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
+      throw e;
+    }
+    dbCleanup(dbFile);
+    _dbConnection = Services.storage.openUnsharedDatabase(dbFile);
+    dbInit();
+  }
+
+  return _dbConnection;
+});
+
+
+var dbStmts = new Map();
+
+/*
+ * dbCreateAsyncStatement
+ *
+ * Creates a statement, wraps it, and then does parameter replacement
+ */
+function dbCreateAsyncStatement(aQuery, aParams, aBindingArrays) {
+  if (!aQuery) {
+    return null;
+  }
+
+  let stmt = dbStmts.get(aQuery);
+  if (!stmt) {
+    log("Creating new statement for query: " + aQuery);
+    stmt = dbConnection.createAsyncStatement(aQuery);
+    dbStmts.set(aQuery, stmt);
+  }
+
+  if (aBindingArrays) {
+    let bindingArray = aBindingArrays.get(stmt);
+    if (!bindingArray) {
+      // first time using a particular statement in update
+      bindingArray = stmt.newBindingParamsArray();
+      aBindingArrays.set(stmt, bindingArray);
+    }
+
+    if (aParams) {
+      let bindingParams = bindingArray.newBindingParams();
+      for (let field in aParams) {
+        bindingParams.bindByName(field, aParams[field]);
+      }
+      bindingArray.addParams(bindingParams);
+    }
+  } else if (aParams) {
+    for (let field in aParams) {
+      stmt.params[field] = aParams[field];
+    }
+  }
+
+  return stmt;
+}
+
+var dbMigrate;
+
+/**
+ * Attempts to initialize the database. This creates the file if it doesn't
+ * exist, performs any migrations, etc.
+ */
+function dbInit() {
+  log("Initializing Database");
+
+  if (!_dbConnection.tableExists("moz_formhistory")) {
+    dbCreate();
+    return;
+  }
+
+  // When FormHistory is released, we will no longer support the various schema versions prior to
+  // this release that nsIFormHistory2 once did.
+  let version = _dbConnection.schemaVersion;
+  if (version < 3) {
+    throw Components.Exception("DB version is unsupported.",
+                               Cr.NS_ERROR_FILE_CORRUPTED);
+  } else if (version != DB_SCHEMA_VERSION) {
+    dbMigrate(version);
+  }
+}
+
 var Migrators = {
   /*
    * Updates the DB schema to v3 (bug 506402).
    * Adds deleted form history table.
    */
-  async dbAsyncMigrateToVersion4(conn) {
-    const TABLE_NAME = "moz_deleted_formhistory";
-    let tableExists = await conn.tableExists(TABLE_NAME);
-    if (!tableExists) {
-      let table = dbSchema.tables[TABLE_NAME];
+  dbMigrateToVersion4() {
+    if (!_dbConnection.tableExists("moz_deleted_formhistory")) {
+      let table = dbSchema.tables.moz_deleted_formhistory;
       let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
-      await conn.execute(`CREATE TABLE ${TABLE_NAME} (${tSQL})`);
+      _dbConnection.createTable("moz_deleted_formhistory", tSQL);
     }
   },
 };
 
-/**
- * @typedef {Object} InsertQueryData
- * @property {Object} updatedChange
- *           A change requested by FormHistory.
- * @property {String} query
- *           The insert query string.
- */
+function dbCreate() {
+  log("Creating DB -- tables");
+  for (let name in dbSchema.tables) {
+    let table = dbSchema.tables[name];
+    let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
+    log("Creating table " + name + " with " + tSQL);
+    _dbConnection.createTable(name, tSQL);
+  }
+
+  log("Creating DB -- indices");
+  for (let name in dbSchema.indices) {
+    let index = dbSchema.indices[name];
+    let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
+                    "(" + index.columns.join(", ") + ")";
+    _dbConnection.executeSimpleSQL(statement);
+  }
+
+  _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+}
+
+dbMigrate = (oldVersion) => {
+  log("Attempting to migrate from version " + oldVersion);
+
+  if (oldVersion > DB_SCHEMA_VERSION) {
+    log("Downgrading to version " + DB_SCHEMA_VERSION);
+    // User's DB is newer. Sanity check that our expected columns are
+    // present, and if so mark the lower version and merrily continue
+    // on. If the columns are borked, something is wrong so blow away
+    // the DB and start from scratch. [Future incompatible upgrades
+    // should switch to a different table or file.]
+
+    if (!dbAreExpectedColumnsPresent()) {
+      throw Components.Exception("DB is missing expected columns",
+                                 Cr.NS_ERROR_FILE_CORRUPTED);
+    }
+
+    // Change the stored version to the current version. If the user
+    // runs the newer code again, it will see the lower version number
+    // and re-upgrade (to fixup any entries the old code added).
+    _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+    return;
+  }
+
+  // Note that migration is currently performed synchronously.
+  _dbConnection.beginTransaction();
+
+  try {
+    for (let v = oldVersion + 1; v <= DB_SCHEMA_VERSION; v++) {
+      this.log("Upgrading to version " + v + "...");
+      Migrators["dbMigrateToVersion" + v]();
+    }
+  } catch (e) {
+    this.log("Migration failed: " + e);
+    this.dbConnection.rollbackTransaction();
+    throw e;
+  }
+
+  _dbConnection.schemaVersion = DB_SCHEMA_VERSION;
+  _dbConnection.commitTransaction();
+
+  log("DB migration completed.");
+};
 
 /**
- * Prepares a query and some default parameters when inserting an entry
- * to the database.
- *
- * @param {Object} change
- *        The change requested by FormHistory.
- * @param {number} now
- *        The current timestamp in microseconds.
- * @returns {InsertQueryData}
- *          The query information needed to pass along to the database.
+ * Sanity check to ensure that the columns this version of the code expects
+ * are present in the DB we're using.
+ * @returns {boolean} whether expected columns are present
+ */
+function dbAreExpectedColumnsPresent() {
+  for (let name in dbSchema.tables) {
+    let table = dbSchema.tables[name];
+    let query = "SELECT " +
+                Object.keys(table).join(", ") +
+                " FROM " + name;
+    try {
+      let stmt = _dbConnection.createStatement(query);
+      // (no need to execute statement, if it compiled we're good)
+      stmt.finalize();
+    } catch (e) {
+      return false;
+    }
+  }
+
+  log("verified that expected columns are present in DB.");
+  return true;
+}
+
+/**
+ * Called when database creation fails. Finalizes database statements,
+ * closes the database connection, deletes the database file.
+ * @param {Object} dbFile database file to close
  */
-function prepareInsertQuery(change, now) {
-  let updatedChange = Object.assign({}, change);
-  let query = "INSERT INTO moz_formhistory " +
-              "(fieldname, value, timesUsed, firstUsed, lastUsed, guid) " +
-              "VALUES (:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)";
-  updatedChange.timesUsed = updatedChange.timesUsed || 1;
-  updatedChange.firstUsed = updatedChange.firstUsed || now;
-  updatedChange.lastUsed = updatedChange.lastUsed || now;
+function dbCleanup(dbFile) {
+  log("Cleaning up DB file - close & remove & backup");
+
+  // Create backup file
+  let backupFile = dbFile.leafName + ".corrupt";
+  Services.storage.backupDatabaseFile(dbFile, backupFile);
+
+  dbClose(false);
+  dbFile.remove(false);
+}
+
+function dbClose(aShutdown) {
+  log("dbClose(" + aShutdown + ")");
+
+  if (aShutdown) {
+    sendNotification("formhistory-shutdown", null);
+  }
 
-  return {
-    updatedChange,
-    query,
-  };
+  // Connection may never have been created if say open failed but we still
+  // end up calling dbClose as part of the rest of dbCleanup.
+  if (!_dbConnection) {
+    return;
+  }
+
+  log("dbClose finalize statements");
+  for (let stmt of dbStmts.values()) {
+    stmt.finalize();
+  }
+
+  dbStmts = new Map();
+
+  let closed = false;
+  _dbConnection.asyncClose(() => closed = true);
+
+  if (!aShutdown) {
+    Services.tm.spinEventLoopUntil(() => closed);
+  }
 }
 
 /**
  * Constructs and executes database statements from a pre-processed list of
  * inputted changes.
  *
  * @param {Array.<Object>} aChanges changes to form history
- * @param {Object} aPreparedHandlers
+ * @param {Object} aCallbacks
  */
-async function updateFormHistoryWrite(aChanges, aPreparedHandlers) {
+async function updateFormHistoryWrite(aChanges, aCallbacks) {
   log("updateFormHistoryWrite  " + aChanges.length);
 
   // pass 'now' down so that every entry in the batch has the same timestamp
   let now = Date.now() * 1000;
-  let queries = [];
+
+  // for each change, we either create and append a new storage statement to
+  // stmts or bind a new set of parameters to an existing storage statement.
+  // stmts and bindingArrays are updated when makeXXXStatement eventually
+  // calls dbCreateAsyncStatement.
+  let stmts = [];
   let notifications = [];
-  let conn = await FormHistory.db;
+  let bindingArrays = new Map();
 
   for (let change of aChanges) {
     let operation = change.op;
     delete change.op;
+    let stmt;
     switch (operation) {
-      case "remove": {
+      case "remove":
         log("Remove from form history  " + change);
-        let queryTerms = makeQueryPredicates(change);
+        let delStmt = makeMoveToDeletedStatement(change.guid, now, change, bindingArrays);
+        if (delStmt && !stmts.includes(delStmt)) {
+          stmts.push(delStmt);
+        }
+        if ("timeDeleted" in change) {
+          delete change.timeDeleted;
+        }
+        stmt = makeRemoveStatement(change, bindingArrays);
 
         // Fetch the GUIDs we are going to delete.
         try {
-          let query = "SELECT guid FROM moz_formhistory";
-          if (queryTerms) {
-            query += " WHERE " + queryTerms;
-          }
-
-          await conn.executeCached(query, change, row => {
-            notifications.push(["formhistory-remove", row.getResultByName("guid")]);
+          await new Promise((res, rej) => {
+            let selectStmt = makeSearchStatement(change, ["guid"]);
+            let selectHandlers = {
+              handleCompletion() {
+                res();
+              },
+              handleError() {
+                log("remove select guids failure");
+              },
+              handleResult(aResultSet) {
+                for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
+                  notifications.push(["formhistory-remove", row.getResultByName("guid")]);
+                }
+              },
+            };
+            dbConnection.executeAsync([selectStmt], 1, selectHandlers);
           });
         } catch (e) {
-          log("Error getting guids from moz_formhistory: " + e);
+          log("Error in select statement: " + e);
         }
 
-        if (supportsDeletedTable) {
-          log("Moving to deleted table " + change);
-          let query = "INSERT INTO moz_deleted_formhistory (guid, timeDeleted)";
-
-          // TODO: Add these items to the deleted items table once we've sorted
-          //       out the issues from bug 756701
-          if (change.guid || queryTerms) {
-            query +=
-              change.guid ? " VALUES (:guid, :timeDeleted)"
-                          : " SELECT guid, :timeDeleted FROM moz_formhistory WHERE " + queryTerms;
-            change.timeDeleted = now;
-            queries.push({ query, params: Object.assign({}, change) });
-          }
-
-          if ("timeDeleted" in change) {
-            delete change.timeDeleted;
-          }
-        }
-
-        let query = "DELETE FROM moz_formhistory";
-        if (queryTerms) {
-          log("removeEntries");
-          query += " WHERE " + queryTerms;
-        } else {
-          log("removeAllEntries");
-          // Not specifying any fields means we should remove all entries. We
-          // won't need to modify the query in this case.
-        }
-
-        queries.push({ query, params: change });
         break;
-      }
-      case "update": {
+      case "update":
         log("Update form history " + change);
         let guid = change.guid;
         delete change.guid;
         // a special case for updating the GUID - the new value can be
         // specified in newGuid.
         if (change.newGuid) {
           change.guid = change.newGuid;
           delete change.newGuid;
         }
-
-        let query = "UPDATE moz_formhistory SET ";
-        let queryTerms = makeQueryPredicates(change, ", ");
-        if (!queryTerms) {
-          throw Components.Exception("Update query must define fields to modify.",
-                                     Cr.NS_ERROR_ILLEGAL_VALUE);
-        }
-        query += queryTerms + " WHERE guid = :existing_guid";
-        change.existing_guid = guid;
-        queries.push({ query, params: change });
+        stmt = makeUpdateStatement(guid, change, bindingArrays);
         notifications.push(["formhistory-update", guid]);
         break;
-      }
-      case "bump": {
+      case "bump":
         log("Bump form history " + change);
         if (change.guid) {
-          let query = "UPDATE moz_formhistory " +
-                      "SET timesUsed = timesUsed + 1, lastUsed = :lastUsed WHERE guid = :guid";
-          let queryParams = {
-            lastUsed: now,
-            guid: change.guid,
-          };
-
-          queries.push({ query, params: queryParams });
+          stmt = makeBumpStatement(change.guid, now, bindingArrays);
           notifications.push(["formhistory-update", change.guid]);
         } else {
           change.guid = generateGUID();
-          let { query, updatedChange } = prepareInsertQuery(change, now);
-          queries.push({ query, params: updatedChange });
-          notifications.push(["formhistory-add", updatedChange.guid]);
+          stmt = makeAddStatement(change, now, bindingArrays);
+          notifications.push(["formhistory-add", change.guid]);
         }
         break;
-      }
-      case "add": {
+      case "add":
         log("Add to form history " + change);
         if (!change.guid) {
           change.guid = generateGUID();
         }
-
-        let { query, updatedChange } = prepareInsertQuery(change, now);
-        queries.push({ query, params: updatedChange });
-        notifications.push(["formhistory-add", updatedChange.guid]);
+        stmt = makeAddStatement(change, now, bindingArrays);
+        notifications.push(["formhistory-add", change.guid]);
         break;
-      }
-      default: {
+      default:
         // We should've already guaranteed that change.op is one of the above
         throw Components.Exception("Invalid operation " + operation,
                                    Cr.NS_ERROR_ILLEGAL_VALUE);
-      }
+    }
+
+    // As identical statements are reused, only add statements if they aren't already present.
+    if (stmt && !stmts.includes(stmt)) {
+      stmts.push(stmt);
     }
   }
 
-  try {
-    await runUpdateQueries(conn, queries);
-  } catch (e) {
-    aPreparedHandlers.handleError(e);
-    aPreparedHandlers.handleCompletion(1);
-    return;
-  }
-
-  for (let [notification, param] of notifications) {
-    // We're either sending a GUID or nothing at all.
-    sendNotification(notification, param);
+  for (let stmt of stmts) {
+    stmt.bindParameters(bindingArrays.get(stmt));
   }
 
-  aPreparedHandlers.handleCompletion(0);
-}
+  let handlers = {
+    handleCompletion(aReason) {
+      if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+        for (let [notification, param] of notifications) {
+          // We're either sending a GUID or nothing at all.
+          sendNotification(notification, param);
+        }
+      }
 
-/**
- * Runs queries for an update operation to the database. This
- * is separated out from updateFormHistoryWrite to avoid shutdown
- * leaks where the handlers passed to updateFormHistoryWrite would
- * leak from the closure around the executeTransaction function.
- *
- * @param {SqliteConnection} conn the database connection
- * @param {Object} queries query string and param pairs generated
- *                 by updateFormHistoryWrite
- */
-async function runUpdateQueries(conn, queries) {
-  await conn.executeTransaction(async () => {
-    for (let { query, params } of queries) {
-      await conn.executeCached(query, params);
-    }
-  });
+      if (aCallbacks && aCallbacks.handleCompletion) {
+        aCallbacks.handleCompletion(
+          aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ?
+            0 :
+            1
+        );
+      }
+    },
+    handleError(aError) {
+      if (aCallbacks && aCallbacks.handleError) {
+        aCallbacks.handleError(aError);
+      }
+    },
+    handleResult: NOOP,
+  };
+
+  dbConnection.executeAsync(stmts, stmts.length, handlers);
 }
 
 /**
  * Functions that expire entries in form history and shrinks database
  * afterwards as necessary initiated by expireOldEntries.
  */
 
 /**
@@ -528,414 +783,142 @@ function expireOldEntriesDeletion(aExpir
  * @param {number} aBeginningCount number of entries at first
  */
 function expireOldEntriesVacuum(aExpireTime, aBeginningCount) {
   FormHistory.count({}, {
     handleResult(aEndingCount) {
       if (aBeginningCount - aEndingCount > 500) {
         log("expireOldEntriesVacuum");
 
-        FormHistory.db.then(async conn => {
-          try {
-            await conn.executeCached("VACUUM");
-          } catch (e) {
+        let stmt = dbCreateAsyncStatement("VACUUM");
+        stmt.executeAsync({
+          handleResult: NOOP,
+          handleError(aError) {
             log("expireVacuumError");
-          }
+          },
+          handleCompletion: NOOP,
         });
       }
 
       sendNotification("formhistory-expireoldentries", aExpireTime);
     },
     handleError(aError) {
       log("expireEndCountFailure");
     },
   });
 }
 
-
-/**
- * Database creation and access. Used by FormHistory and some of the
- * utility functions, but is not exposed to the outside world.
- * @class
- */
-this.DB = {
-  // Once we establish a database connection, we have to hold a reference
-  // to it so that it won't get GC'd.
-  _instance: null,
-  // MAX_ATTEMPTS is how many times we'll try to establish a connection
-  // or migrate a database before giving up.
-  MAX_ATTEMPTS: 2,
-
-  /** String representing where the FormHistory database is on the filesystem */
-  get path() {
-    return OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
-  },
-
-  /**
-   * Sets up and returns a connection to the FormHistory database. The
-   * connection also registers itself with AsyncShutdown so that the
-   * connection is closed on when the profile-before-change observer
-   * notification is fired.
-   *
-   * @returns {Promise}
-   * @resolves An Sqlite.jsm connection to the database.
-   * @rejects  If connecting to the database, or migrating the database
-   *           failed after MAX_ATTEMPTS attempts (where each attempt
-   *           backs up and deletes the old database), this will reject
-   *           with the Sqlite.jsm error.
-   */
-  get conn() {
-    delete this.conn;
-    let conn = new Promise(async (resolve, reject) => {
-      try {
-        this._instance = await this._establishConn();
-      } catch (e) {
-        log("Failed to establish database connection.");
-        reject(e);
-        return;
-      }
-
-      AsyncShutdown.profileBeforeChange.addBlocker(
-        "Closing FormHistory database.", () => this._instance.close());
-
-      resolve(this._instance);
-    });
-
-    return this.conn = conn;
-  },
-
-  // Private functions
-
-  /**
-   * Tries to connect to the Sqlite database at this.path, and then
-   * migrates the database as necessary. If any of the steps to do this
-   * fail, this function should re-enter itself with an incremented
-   * attemptNum so that another attempt can be made after backing up
-   * and deleting the old database.
-   *
-   * @async
-   * @param {number} attemptNum
-   *        The optional number of the attempt that is being made to connect
-   *        to the database. Defaults to 0.
-   * @returns {Promise}
-   * @resolves An Sqlite.jsm connection to the database.
-   * @rejects  After MAX_ATTEMPTS, this will reject with the Sqlite.jsm
-   *           error.
-   */
-  async _establishConn(attemptNum = 0) {
-    log(`Establishing database connection - attempt # ${attemptNum}`);
-    let conn;
-    try {
-      conn = await Sqlite.openConnection({ path: this.path });
-    } catch (e) {
-      // Bug 1423729 - We should check the reason for the connection failure,
-      // in case this is due to the disk being full or the database file being
-      // inaccessible due to third-party software (like anti-virus software).
-      // In that case, we should probably fail right away.
-      if (attemptNum < this.MAX_ATTEMPTS) {
-        log("Establishing connection failed.");
-        await this._failover(conn);
-        return this._establishConn(++attemptNum);
-      }
-
-      if (conn) {
-        await conn.close();
-      }
-      log("Establishing connection failed too many times. Giving up.");
-      throw e;
-    }
-
-    try {
-      let dbVersion = parseInt(await conn.getSchemaVersion(), 10);
-
-      // Case 1: Database is up to date and we're ready to go.
-      if (dbVersion == DB_SCHEMA_VERSION) {
-        return conn;
-      }
-
-      // Case 2: Downgrade
-      if (dbVersion > DB_SCHEMA_VERSION) {
-        log("Downgrading to version " + DB_SCHEMA_VERSION);
-        // User's DB is newer. Sanity check that our expected columns are
-        // present, and if so mark the lower version and merrily continue
-        // on. If the columns are borked, something is wrong so blow away
-        // the DB and start from scratch. [Future incompatible upgrades
-        // should switch to a different table or file.]
-        if (!(await this._expectedColumnsPresent(conn))) {
-          throw Components.Exception("DB is missing expected columns",
-                                     Cr.NS_ERROR_FILE_CORRUPTED);
-        }
-
-        // Change the stored version to the current version. If the user
-        // runs the newer code again, it will see the lower version number
-        // and re-upgrade (to fixup any entries the old code added).
-        await conn.setSchemaVersion(DB_SCHEMA_VERSION);
-        return conn;
-      }
-
-      // Case 3: Very old database that cannot be migrated.
-      //
-      // When FormHistory is released, we will no longer support the various
-      // schema versions prior to this release that nsIFormHistory2 once did.
-      // We'll throw an NS_ERROR_FILE_CORRUPTED, which should cause us to wipe
-      // out this DB and create a new one (unless this is our MAX_ATTEMPTS
-      // attempt).
-      if (dbVersion > 0 && dbVersion < 3) {
-        throw Components.Exception("DB version is unsupported.",
-                                   Cr.NS_ERROR_FILE_CORRUPTED);
-      }
-
-      if (dbVersion == 0) {
-        // Case 4: New database
-        await conn.executeTransaction(async () => {
-          log("Creating DB -- tables");
-          for (let name in dbSchema.tables) {
-            let table = dbSchema.tables[name];
-            let tSQL = Object.keys(table).map(col => [col, table[col]].join(" ")).join(", ");
-            log("Creating table " + name + " with " + tSQL);
-            await conn.execute(`CREATE TABLE ${name} (${tSQL})`);
-          }
-
-          log("Creating DB -- indices");
-          for (let name in dbSchema.indices) {
-            let index = dbSchema.indices[name];
-            let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
-                            "(" + index.columns.join(", ") + ")";
-            await conn.execute(statement);
-          }
-        });
-      } else {
-        // Case 5: Old database requiring a migration
-        await conn.executeTransaction(async () => {
-          for (let v = dbVersion + 1; v <= DB_SCHEMA_VERSION; v++) {
-            log("Upgrading to version " + v + "...");
-            await Migrators["dbAsyncMigrateToVersion" + v](conn);
-          }
-        });
-      }
-
-      await conn.setSchemaVersion(DB_SCHEMA_VERSION);
-
-      return conn;
-    } catch (e) {
-      if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) {
-        throw e;
-      }
-
-      if (attemptNum < this.MAX_ATTEMPTS) {
-        log("Setting up database failed.");
-        await this._failover(conn);
-        return this._establishConn(++attemptNum);
-      }
-
-      if (conn) {
-        await conn.close();
-      }
-
-      log("Setting up database failed too many times. Giving up.");
-
-      throw e;
-    }
-  },
-
-  /**
-   * Closes a connection to the database, then backs up the database before
-   * deleting it.
-   *
-   * @async
-   * @param {SqliteConnection | null} conn
-   *        The connection to the database that we failed to establish or
-   *        migrate.
-   * @throws If any file operations fail.
-   */
-  async _failover(conn) {
-    log("Cleaning up DB file - close & remove & backup.");
-    if (conn) {
-      await conn.close();
-    }
-    let backupFile = this.path + ".corrupt";
-    let { file, path: uniquePath } =
-      await OS.File.openUnique(backupFile, { humanReadable: true });
-    await file.close();
-    await OS.File.copy(this.path, uniquePath);
-    await OS.File.remove(this.path);
-    log("Completed DB cleanup.");
-  },
-
-  /**
-   * Tests that a database connection contains the tables that we expect.
-   *
-   * @async
-   * @param {SqliteConnection | null} conn
-   *        The connection to the database that we're testing.
-   * @returns {Promise}
-   * @resolves true if all expected columns are present.
-   */
-  async _expectedColumnsPresent(conn) {
-    for (let name in dbSchema.tables) {
-      let table = dbSchema.tables[name];
-      let query = "SELECT " +
-                  Object.keys(table).join(", ") +
-                  " FROM " + name;
-      try {
-        await conn.execute(query, null, (row, cancel) => {
-          // One row is enough to let us know this worked.
-          cancel();
-        });
-      } catch (e) {
-        return false;
-      }
-    }
-
-    log("Verified that expected columns are present in DB.");
-    return true;
-  },
-};
-
 this.FormHistory = {
-  get db() {
-    return DB.conn;
-  },
-
   get enabled() {
     return Prefs.enabled;
   },
 
-  _prepareHandlers(handlers) {
-    let defaultHandlers = {
-      handleResult: NOOP,
-      handleError: NOOP,
-      handleCompletion: NOOP,
-    };
-
-    if (!handlers) {
-      return defaultHandlers;
-    }
-
-    if (handlers.handleResult) {
-      defaultHandlers.handleResult = handlers.handleResult;
-    }
-    if (handlers.handleError) {
-      defaultHandlers.handleError = handlers.handleError;
-    }
-    if (handlers.handleCompletion) {
-      defaultHandlers.handleCompletion = handlers.handleCompletion;
-    }
-
-    return defaultHandlers;
-  },
-
-  search(aSelectTerms, aSearchData, aRowFuncOrHandlers) {
+  search(aSelectTerms, aSearchData, aCallbacks) {
     // if no terms selected, select everything
     if (!aSelectTerms) {
       aSelectTerms = validFields;
     }
     validateSearchData(aSearchData, "Search");
 
-    let query = "SELECT " + aSelectTerms.join(", ") + " FROM moz_formhistory";
-    let queryTerms = makeQueryPredicates(aSearchData);
-    if (queryTerms) {
-      query += " WHERE " + queryTerms;
-    }
-
-    let handlers;
+    let stmt = makeSearchStatement(aSearchData, aSelectTerms);
 
-    if (typeof aRowFuncOrHandlers == "function") {
-      handlers = this._prepareHandlers();
-      handlers.handleResult = aRowFuncOrHandlers;
-    } else if (typeof aRowFuncOrHandlers == "object") {
-      handlers = this._prepareHandlers(aRowFuncOrHandlers);
-    }
+    let handlers = {
+      handleResult(aResultSet) {
+        for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
+          let result = {};
+          for (let field of aSelectTerms) {
+            result[field] = row.getResultByName(field);
+          }
 
-    let allResults = [];
+          if (aCallbacks && aCallbacks.handleResult) {
+            aCallbacks.handleResult(result);
+          }
+        }
+      },
 
-    return new Promise((resolve, reject) => {
-      this.db.then(async conn => {
-        try {
-          await conn.executeCached(query, aSearchData, row => {
-            let result = {};
-            for (let field of aSelectTerms) {
-              result[field] = row.getResultByName(field);
-            }
+      handleError(aError) {
+        if (aCallbacks && aCallbacks.handleError) {
+          aCallbacks.handleError(aError);
+        }
+      },
 
-            if (handlers) {
-              handlers.handleResult(result);
-            } else {
-              allResults.push(result);
-            }
-          });
-          if (handlers) {
-            handlers.handleCompletion(0);
-          }
-          resolve(allResults);
-        } catch (e) {
-          if (handlers) {
-            handlers.handleError(e);
-            handlers.handleCompletion(1);
-          }
-          reject(e);
+      handleCompletion(aReason) {
+        if (aCallbacks && aCallbacks.handleCompletion) {
+          aCallbacks.handleCompletion(
+            aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ?
+              0 :
+              1
+          );
         }
-      });
-    });
+      },
+    };
+
+    stmt.executeAsync(handlers);
   },
 
-  count(aSearchData, aHandlers) {
+  count(aSearchData, aCallbacks) {
     validateSearchData(aSearchData, "Count");
-
-    let query = "SELECT COUNT(*) AS numEntries FROM moz_formhistory";
-    let queryTerms = makeQueryPredicates(aSearchData);
-    if (queryTerms) {
-      query += " WHERE " + queryTerms;
-    }
-
-    let handlers = this._prepareHandlers(aHandlers);
+    let stmt = makeCountStatement(aSearchData);
+    let handlers = {
+      handleResult(aResultSet) {
+        let row = aResultSet.getNextRow();
+        let count = row.getResultByName("numEntries");
+        if (aCallbacks && aCallbacks.handleResult) {
+          aCallbacks.handleResult(count);
+        }
+      },
 
-    return new Promise((resolve, reject) => {
-      this.db.then(async conn => {
-        try {
-          let rows = await conn.executeCached(query, aSearchData);
-          let count = rows[0].getResultByName("numEntries");
-          handlers.handleResult(count);
-          handlers.handleCompletion(0);
-          resolve(count);
-        } catch (e) {
-          handlers.handleError(e);
-          handlers.handleCompletion(1);
-          reject(e);
+      handleError(aError) {
+        if (aCallbacks && aCallbacks.handleError) {
+          aCallbacks.handleError(aError);
         }
-      });
-    });
+      },
+
+      handleCompletion(aReason) {
+        if (aCallbacks && aCallbacks.handleCompletion) {
+          aCallbacks.handleCompletion(
+            aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ?
+              0 :
+              1
+          );
+        }
+      },
+    };
+
+    stmt.executeAsync(handlers);
   },
 
-  update(aChanges, aHandlers) {
+  update(aChanges, aCallbacks) {
     // Used to keep track of how many searches have been started. When that number
     // are finished, updateFormHistoryWrite can be called.
     let numSearches = 0;
     let completedSearches = 0;
     let searchFailed = false;
 
     function validIdentifier(change) {
       // The identifier is only valid if one of either the guid
       // or the (fieldname/value) are set (so an X-OR)
       return Boolean(change.guid) != Boolean(change.fieldname && change.value);
     }
 
     if (!("length" in aChanges)) {
       aChanges = [aChanges];
     }
 
-    let handlers = this._prepareHandlers(aHandlers);
-
     let isRemoveOperation = aChanges.every(change => change && change.op && change.op == "remove");
     if (!Prefs.enabled && !isRemoveOperation) {
-      handlers.handleError({
-        message: "Form history is disabled, only remove operations are allowed",
-        result: Ci.mozIStorageError.MISUSE,
-      });
-      handlers.handleCompletion(1);
+      if (aCallbacks && aCallbacks.handleError) {
+        aCallbacks.handleError({
+          message: "Form history is disabled, only remove operations are allowed",
+          result: Ci.mozIStorageError.MISUSE,
+        });
+      }
+      if (aCallbacks && aCallbacks.handleCompletion) {
+        aCallbacks.handleCompletion(1);
+      }
       return;
     }
 
     for (let change of aChanges) {
       switch (change.op) {
         case "remove":
           validateSearchData(change, "Remove");
           continue;
@@ -985,78 +968,72 @@ this.FormHistory = {
         {
           fieldname: change.fieldname,
           value: change.value,
         }, {
           foundResult: false,
           handleResult(aResult) {
             if (this.foundResult) {
               log("Database contains multiple entries with the same fieldname/value pair.");
-              handlers.handleError({
-                message:
-                  "Database contains multiple entries with the same fieldname/value pair.",
-                result: 19, // Constraint violation
-              });
+              if (aCallbacks && aCallbacks.handleError) {
+                aCallbacks.handleError({
+                  message:
+                    "Database contains multiple entries with the same fieldname/value pair.",
+                  result: 19, // Constraint violation
+                });
+              }
 
               searchFailed = true;
               return;
             }
 
             this.foundResult = true;
             changeToUpdate.guid = aResult.guid;
           },
 
           handleError(aError) {
-            handlers.handleError(aError);
+            if (aCallbacks && aCallbacks.handleError) {
+              aCallbacks.handleError(aError);
+            }
           },
 
           handleCompletion(aReason) {
             completedSearches++;
             if (completedSearches == numSearches) {
               if (!aReason && !searchFailed) {
-                updateFormHistoryWrite(aChanges, handlers);
-              } else {
-                handlers.handleCompletion(1);
+                updateFormHistoryWrite(aChanges, aCallbacks);
+              } else if (aCallbacks && aCallbacks.handleCompletion) {
+                aCallbacks.handleCompletion(1);
               }
             }
           },
         });
     }
 
     if (numSearches == 0) {
       // We don't have to wait for any statements to return.
-      updateFormHistoryWrite(aChanges, handlers);
+      updateFormHistoryWrite(aChanges, aCallbacks);
     }
   },
 
-  getAutoCompleteResults(searchString, params, aHandlers) {
+  getAutoCompleteResults(searchString, params, aCallbacks) {
     // only do substring matching when the search string contains more than one character
     let searchTokens;
     let where = "";
     let boundaryCalc = "";
-
-    if (searchString.length >= 1) {
-      params.valuePrefix = searchString + "%";
-    }
-
     if (searchString.length > 1) {
       searchTokens = searchString.split(/\s+/);
 
       // build up the word boundary and prefix match bonus calculation
       boundaryCalc = "MAX(1, :prefixWeight * (value LIKE :valuePrefix ESCAPE '/') + (";
       // for each word, calculate word boundary weights for the SELECT clause and
       // add word to the WHERE clause of the query
       let tokenCalc = [];
       let searchTokenCount = Math.min(searchTokens.length, MAX_SEARCH_TOKENS);
       for (let i = 0; i < searchTokenCount; i++) {
-        let escapedToken = searchTokens[i];
-        params["tokenBegin" + i] = escapedToken + "%";
-        params["tokenBoundary" + i] = "% " + escapedToken + "%";
-        params["tokenContains" + i] = "%" + escapedToken + "%";
-
         tokenCalc.push("(value LIKE :tokenBegin" + i + " ESCAPE '/') + " +
                             "(value LIKE :tokenBoundary" + i + " ESCAPE '/')");
         where += "AND (value LIKE :tokenContains" + i + " ESCAPE '/') ";
       }
       // add more weight if we have a traditional prefix match and
       // multiply boundary bonuses by boundary weight
       boundaryCalc += tokenCalc.join(" + ") + ") * :boundaryWeight)";
     } else if (searchString.length == 1) {
@@ -1068,18 +1045,16 @@ this.FormHistory = {
       where = "";
       boundaryCalc = "1";
       delete params.prefixWeight;
       delete params.boundaryWeight;
     }
 
     params.now = Date.now() * 1000; // convert from ms to microseconds
 
-    let handlers = this._prepareHandlers(aHandlers);
-
     /* Three factors in the frecency calculation for an entry (in order of use in calculation):
      * 1) average number of times used - items used more are ranked higher
      * 2) how recently it was last used - items used recently are ranked higher
      * 3) additional weight for aged entries surviving expiry - these entries are relevant
      *    since they have been used multiple times over a large time span so rank them higher
      * The score is then divided by the bucket size and we round the result so that entries
      * with a very similar frecency are bucketed together with an alphabetical sort. This is
      * to reduce the amount of moving around by entries while typing.
@@ -1093,52 +1068,76 @@ this.FormHistory = {
                     "MAX(1.0, :agedWeight * (firstUsed < :expiryDate)) / " +
                     ":bucketSize " +
                 ", 3) AS frecency, " +
                 boundaryCalc + " AS boundaryBonuses " +
                 "FROM moz_formhistory " +
                 "WHERE fieldname=:fieldname " + where +
                 "ORDER BY ROUND(frecency * boundaryBonuses) DESC, UPPER(value) ASC";
 
-    let cancelled = false;
-
-    let cancellableQuery = {
-      cancel() {
-        cancelled = true;
-      },
-    };
+    let stmt = dbCreateAsyncStatement(query, params);
 
-    this.db.then(async conn => {
-      try {
-        await conn.executeCached(query, params, (row, cancel) => {
-          if (cancelled) {
-            cancel();
-            return;
-          }
+    // Chicken and egg problem: Need the statement to escape the params we
+    // pass to the function that gives us the statement. So, fix it up now.
+    if (searchString.length >= 1) {
+      stmt.params.valuePrefix = stmt.escapeStringForLIKE(searchString, "/") + "%";
+    }
+    if (searchString.length > 1) {
+      let searchTokenCount = Math.min(searchTokens.length, MAX_SEARCH_TOKENS);
+      for (let i = 0; i < searchTokenCount; i++) {
+        let escapedToken = stmt.escapeStringForLIKE(searchTokens[i], "/");
+        stmt.params["tokenBegin" + i] = escapedToken + "%";
+        stmt.params["tokenBoundary" + i] =  "% " + escapedToken + "%";
+        stmt.params["tokenContains" + i] = "%" + escapedToken + "%";
+      }
+    } else {
+      // no additional params need to be substituted into the query when the
+      // length is zero or one
+    }
 
+    let pending = stmt.executeAsync({
+      handleResult(aResultSet) {
+        for (let row = aResultSet.getNextRow(); row; row = aResultSet.getNextRow()) {
           let value = row.getResultByName("value");
           let guid = row.getResultByName("guid");
           let frecency = row.getResultByName("frecency");
           let entry = {
-            text: value,
+            text:          value,
             guid,
             textLowerCase: value.toLowerCase(),
             frecency,
-            totalScore: Math.round(frecency * row.getResultByName("boundaryBonuses")),
+            totalScore:    Math.round(frecency * row.getResultByName("boundaryBonuses")),
           };
-          handlers.handleResult(entry);
-        });
-        handlers.handleCompletion(0);
-      } catch (e) {
-        handlers.handleError(e);
-        handlers.handleCompletion(1);
-      }
+          if (aCallbacks && aCallbacks.handleResult) {
+            aCallbacks.handleResult(entry);
+          }
+        }
+      },
+
+      handleError(aError) {
+        if (aCallbacks && aCallbacks.handleError) {
+          aCallbacks.handleError(aError);
+        }
+      },
+
+      handleCompletion(aReason) {
+        if (aCallbacks && aCallbacks.handleCompletion) {
+          aCallbacks.handleCompletion(
+            aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED ?
+              0 :
+              1
+          );
+        }
+      },
     });
+    return pending;
+  },
 
-    return cancellableQuery;
+  get schemaVersion() {
+    return dbConnection.schemaVersion;
   },
 
   // This is used only so that the test can verify deleted table support.
   get _supportsDeletedTable() {
     return supportsDeletedTable;
   },
   set _supportsDeletedTable(val) {
     supportsDeletedTable = val;
@@ -1162,12 +1161,14 @@ this.FormHistory = {
       handleResult(aBeginningCount) {
         expireOldEntriesDeletion(expireTime, aBeginningCount);
       },
       handleError(aError) {
         log("expireStartCountFailure");
       },
     });
   },
+
+  shutdown() { dbClose(true); },
 };
 
 // Prevent add-ons from redefining this API
 Object.freeze(FormHistory);
--- a/toolkit/components/satchel/FormHistoryStartup.js
+++ b/toolkit/components/satchel/FormHistoryStartup.js
@@ -25,16 +25,19 @@ FormHistoryStartup.prototype = {
     switch (topic) {
       case "nsPref:changed":
         FormHistory.updatePrefs();
         break;
       case "idle-daily":
       case "formhistory-expire-now":
         FormHistory.expireOldEntries();
         break;
+      case "profile-before-change":
+        FormHistory.shutdown();
+        break;
       case "profile-after-change":
         this.init();
         break;
     }
   },
 
   inited: false,
   pendingQuery: null,
@@ -43,16 +46,17 @@ FormHistoryStartup.prototype = {
     if (this.inited) {
       return;
     }
     this.inited = true;
 
     Services.prefs.addObserver("browser.formfill.", this, true);
 
     // triggers needed service cleanup and db shutdown
+    Services.obs.addObserver(this, "profile-before-change", true);
     Services.obs.addObserver(this, "idle-daily", true);
     Services.obs.addObserver(this, "formhistory-expire-now", true);
 
     Services.ppmm.loadProcessScript("chrome://satchel/content/formSubmitListener.js", true);
     Services.ppmm.addMessageListener("FormHistory:FormSubmitEntries", this);
 
     let messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
                          .getService(Ci.nsIMessageListenerManager);
--- a/toolkit/components/satchel/test/unit/head_satchel.js
+++ b/toolkit/components/satchel/test/unit/head_satchel.js
@@ -29,25 +29,16 @@ formHistoryStartup.observe(null, "profil
 function getDBVersion(dbfile) {
   let dbConnection = Services.storage.openDatabase(dbfile);
   let version = dbConnection.schemaVersion;
   dbConnection.close();
 
   return version;
 }
 
-function getFormHistoryDBVersion() {
-  let profileDir = do_get_profile();
-  // Cleanup from any previous tests or failures.
-  let dbFile = profileDir.clone();
-  dbFile.append("formhistory.sqlite");
-  return getDBVersion(dbFile);
-}
-
-
 const isGUID = /[A-Za-z0-9\+\/]{16}/;
 
 // Find form history entries.
 function searchEntries(terms, params, iter) {
   let results = [];
   FormHistory.search(terms, params, {
     handleResult: result => results.push(result),
     handleError(error) {
@@ -106,19 +97,19 @@ function addEntry(name, value, then) {
     fieldname: name,
     value,
     timesUsed: 1,
     firstUsed: now,
     lastUsed: now,
   }, then);
 }
 
-function promiseCountEntries(name, value, checkFn = () => {}) {
-  return new Promise(resolve => {
-    countEntries(name, value, function(result) { checkFn(result); resolve(result); });
+function promiseCountEntries(name, value) {
+  return new Promise(res => {
+    countEntries(name, value, res);
   });
 }
 
 function promiseUpdateEntry(op, name, value) {
   return new Promise(res => {
     updateEntry(op, name, value, res);
   });
 }
@@ -138,33 +129,16 @@ function updateFormHistory(changes, then
     handleCompletion(reason) {
       if (!reason) {
         then();
       }
     },
   });
 }
 
-function promiseUpdate(change) {
-  return new Promise((resolve, reject) => {
-    FormHistory.update(change, {
-      handleError(error) {
-        this._error = error;
-      },
-      handleCompletion(reason) {
-        if (reason) {
-          reject(this._error);
-        } else {
-          resolve();
-        }
-      },
-    });
-  });
-}
-
 /**
  * Logs info to the console in the standard way (includes the filename).
  *
  * @param {string} aMessage
  *        The message to log to the console.
  */
 function do_log_info(aMessage) {
   print("TEST-INFO | " + _TEST_FILE + " | " + aMessage);
--- a/toolkit/components/satchel/test/unit/test_async_expire.js
+++ b/toolkit/components/satchel/test/unit/test_async_expire.js
@@ -1,130 +1,165 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
-                                  "resource://testing-common/TestUtils.jsm");
+var dbFile;
 
-function promiseExpiration() {
-  let promise = TestUtils.topicObserved("satchel-storage-changed", (subject, data) => {
-    return data == "formhistory-expireoldentries";
-  });
-
+function triggerExpiration() {
   // We can't easily fake a "daily idle" event, so for testing purposes form
   // history listens for another notification to trigger an immediate
   // expiration.
   Services.obs.notifyObservers(null, "formhistory-expire-now");
-
-  return promise;
 }
 
-add_task(async function() {
+var checkExists = function(num) { Assert.ok(num > 0); next_test(); };
+var checkNotExists = function(num) { Assert.ok(!num); next_test(); };
+
+var TestObserver = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+  observe(subject, topic, data) {
+    Assert.equal(topic, "satchel-storage-changed");
+
+    if (data == "formhistory-expireoldentries") {
+      next_test();
+    }
+  },
+};
+
+function test_finished() {
+  // Make sure we always reset prefs.
+  if (Services.prefs.prefHasUserValue("browser.formfill.expire_days")) {
+    Services.prefs.clearUserPref("browser.formfill.expire_days");
+  }
+
+  do_test_finished();
+}
+
+var iter = tests();
+
+function run_test() {
+  do_test_pending();
+  iter.next();
+}
+
+function next_test() {
+  iter.next();
+}
+
+function* tests() {
+  Services.obs.addObserver(TestObserver, "satchel-storage-changed", true);
+
   // ===== test init =====
   let testfile = do_get_file("asyncformhistory_expire.sqlite");
   let profileDir = do_get_profile();
 
   // Cleanup from any previous tests or failures.
-  let dbFile = profileDir.clone();
+  dbFile = profileDir.clone();
   dbFile.append("formhistory.sqlite");
   if (dbFile.exists()) {
     dbFile.remove(false);
   }
 
   testfile.copyTo(profileDir, "formhistory.sqlite");
   Assert.ok(dbFile.exists());
 
   // We're going to clear this at the end, so it better have the default value now.
   Assert.ok(!Services.prefs.prefHasUserValue("browser.formfill.expire_days"));
 
   // Sanity check initial state
-  Assert.equal(508, await promiseCountEntries(null, null));
-  Assert.ok(await promiseCountEntries("name-A", "value-A") > 0); // lastUsed == distant past
-  Assert.ok(await promiseCountEntries("name-B", "value-B") > 0); // lastUsed == distant future
+  yield countEntries(null, null, function(num) { Assert.equal(508, num); next_test(); });
+  yield countEntries("name-A", "value-A", checkExists); // lastUsed == distant past
+  yield countEntries("name-B", "value-B", checkExists); // lastUsed == distant future
 
-  Assert.equal(CURRENT_SCHEMA, getDBVersion(dbFile));
+  Assert.equal(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
   // Add a new entry
-  Assert.equal(0, await promiseCountEntries("name-C", "value-C"));
-  await promiseAddEntry("name-C", "value-C");
-  Assert.equal(1, await promiseCountEntries("name-C", "value-C"));
+  yield countEntries("name-C", "value-C", checkNotExists);
+  yield addEntry("name-C", "value-C", next_test);
+  yield countEntries("name-C", "value-C", checkExists);
 
   // Update some existing entries to have ages relative to when the test runs.
   let now = 1000 * Date.now();
-  let updateLastUsed = (results, age) => {
+  let updateLastUsed = function updateLastUsedFn(results, age) {
     let lastUsed = now - age * 24 * PR_HOURS;
 
     let changes = [];
-    for (let result of results) {
-      changes.push({ op: "update", lastUsed, guid: result.guid });
+    for (let r = 0; r < results.length; r++) {
+      changes.push({ op: "update", lastUsed, guid: results[r].guid });
     }
 
     return changes;
   };
 
-  let results = await FormHistory.search(["guid"], { lastUsed: 181 });
-  await promiseUpdate(updateLastUsed(results, 181));
+  let results = yield searchEntries(["guid"], { lastUsed: 181 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 181), next_test);
 
-  results = await FormHistory.search(["guid"], { lastUsed: 179 });
-  await promiseUpdate(updateLastUsed(results, 179));
+  results = yield searchEntries(["guid"], { lastUsed: 179 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 179), next_test);
 
-  results = await FormHistory.search(["guid"], { lastUsed: 31 });
-  await promiseUpdate(updateLastUsed(results, 31));
+  results = yield searchEntries(["guid"], { lastUsed: 31 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 31), next_test);
 
-  results = await FormHistory.search(["guid"], { lastUsed: 29 });
-  await promiseUpdate(updateLastUsed(results, 29));
+  results = yield searchEntries(["guid"], { lastUsed: 29 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 29), next_test);
 
-  results = await FormHistory.search(["guid"], { lastUsed: 9999 });
-  await promiseUpdate(updateLastUsed(results, 11));
+  results = yield searchEntries(["guid"], { lastUsed: 9999 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 11), next_test);
 
-  results = await FormHistory.search(["guid"], { lastUsed: 9 });
-  await promiseUpdate(updateLastUsed(results, 9));
+  results = yield searchEntries(["guid"], { lastUsed: 9 }, iter);
+  yield updateFormHistory(updateLastUsed(results, 9), next_test);
 
-  Assert.ok(await promiseCountEntries("name-A", "value-A") > 0);
-  Assert.ok(await promiseCountEntries("181DaysOld", "foo") > 0);
-  Assert.ok(await promiseCountEntries("179DaysOld", "foo") > 0);
-  Assert.equal(509, await promiseCountEntries(null, null));
+  yield countEntries("name-A", "value-A", checkExists);
+  yield countEntries("181DaysOld", "foo", checkExists);
+  yield countEntries("179DaysOld", "foo", checkExists);
+  yield countEntries(null, null, function(num) { Assert.equal(509, num); next_test(); });
 
   // 2 entries are expected to expire.
-  await promiseExpiration();
+  triggerExpiration();
+  yield;
 
-  Assert.equal(0, await promiseCountEntries("name-A", "value-A"));
-  Assert.equal(0, await promiseCountEntries("181DaysOld", "foo"));
-  Assert.ok(await promiseCountEntries("179DaysOld", "foo") > 0);
-  Assert.equal(507, await promiseCountEntries(null, null));
+  yield countEntries("name-A", "value-A", checkNotExists);
+  yield countEntries("181DaysOld", "foo", checkNotExists);
+  yield countEntries("179DaysOld", "foo", checkExists);
+  yield countEntries(null, null, function(num) { Assert.equal(507, num); next_test(); });
 
   // And again. No change expected.
-  await promiseExpiration();
+  triggerExpiration();
+  yield;
 
-  Assert.equal(507, await promiseCountEntries(null, null));
+  yield countEntries(null, null, function(num) { Assert.equal(507, num); next_test(); });
 
   // Set formfill pref to 30 days.
   Services.prefs.setIntPref("browser.formfill.expire_days", 30);
-
-  Assert.ok(await promiseCountEntries("179DaysOld", "foo") > 0);
-  Assert.ok(await promiseCountEntries("bar", "31days") > 0);
-  Assert.ok(await promiseCountEntries("bar", "29days") > 0);
-  Assert.equal(507, await promiseCountEntries(null, null));
+  yield countEntries("179DaysOld", "foo", checkExists);
+  yield countEntries("bar", "31days", checkExists);
+  yield countEntries("bar", "29days", checkExists);
+  yield countEntries(null, null, function(num) { Assert.equal(507, num); next_test(); });
 
-  await promiseExpiration();
+  triggerExpiration();
+  yield;
 
-  Assert.equal(0, await promiseCountEntries("179DaysOld", "foo"));
-  Assert.equal(0, await promiseCountEntries("bar", "31days"));
-  Assert.ok(await promiseCountEntries("bar", "29days") > 0);
-  Assert.equal(505, await promiseCountEntries(null, null));
+  yield countEntries("179DaysOld", "foo", checkNotExists);
+  yield countEntries("bar", "31days", checkNotExists);
+  yield countEntries("bar", "29days", checkExists);
+  yield countEntries(null, null, function(num) { Assert.equal(505, num); next_test(); });
 
   // Set override pref to 10 days and expire. This expires a large batch of
   // entries, and should trigger a VACCUM to reduce file size.
   Services.prefs.setIntPref("browser.formfill.expire_days", 10);
 
-  Assert.ok(await promiseCountEntries("bar", "29days") > 0);
-  Assert.ok(await promiseCountEntries("9DaysOld", "foo") > 0);
-  Assert.equal(505, await promiseCountEntries(null, null));
+  yield countEntries("bar", "29days", checkExists);
+  yield countEntries("9DaysOld", "foo", checkExists);
+  yield countEntries(null, null, function(num) { Assert.equal(505, num); next_test(); });
 
-  await promiseExpiration();
+  triggerExpiration();
+  yield;
 
-  Assert.equal(0, await promiseCountEntries("bar", "29days"));
-  Assert.ok(await promiseCountEntries("9DaysOld", "foo") > 0);
-  Assert.ok(await promiseCountEntries("name-B", "value-B") > 0);
-  Assert.ok(await promiseCountEntries("name-C", "value-C") > 0);
-  Assert.equal(3, await promiseCountEntries(null, null));
-});
+  yield countEntries("bar", "29days", checkNotExists);
+  yield countEntries("9DaysOld", "foo", checkExists);
+  yield countEntries("name-B", "value-B", checkExists);
+  yield countEntries("name-C", "value-C", checkExists);
+  yield countEntries(null, null, function(num) { Assert.equal(3, num); next_test(); });
+
+  test_finished();
+}
--- a/toolkit/components/satchel/test/unit/test_db_update_v4.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4.js
@@ -1,52 +1,57 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-add_task(async function() {
-  let testnum = 0;
+var testnum = 0;
+
+var iter;
 
+function run_test() {
+  do_test_pending();
+  iter = next_test();
+  iter.next();
+}
+
+function* next_test() {
   try {
-    // ===== test init =====
+  // ===== test init =====
     let testfile = do_get_file("formhistory_v3.sqlite");
     let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
 
     // Cleanup from any previous tests or failures.
     let destFile = profileDir.clone();
     destFile.append("formhistory.sqlite");
     if (destFile.exists()) {
       destFile.remove(false);
     }
 
     testfile.copyTo(profileDir, "formhistory.sqlite");
     Assert.equal(3, getDBVersion(testfile));
 
-    Assert.ok(destFile.exists());
-
     // ===== 1 =====
     testnum++;
 
     destFile = profileDir.clone();
     destFile.append("formhistory.sqlite");
     let dbConnection = Services.storage.openUnsharedDatabase(destFile);
 
-    // Do something that will cause FormHistory to access and upgrade the
-    // database
-    await FormHistory.count({});
-
     // check for upgraded schema.
-    Assert.equal(CURRENT_SCHEMA, getDBVersion(destFile));
+    Assert.equal(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
     // Check that the index was added
     Assert.ok(dbConnection.tableExists("moz_deleted_formhistory"));
     dbConnection.close();
 
     // check for upgraded schema.
-    Assert.equal(CURRENT_SCHEMA, getDBVersion(destFile));
-
+    Assert.equal(CURRENT_SCHEMA, FormHistory.schemaVersion);
     // check that an entry still exists
-    let num = await promiseCountEntries("name-A", "value-A");
-    Assert.ok(num > 0);
+    yield countEntries("name-A", "value-A",
+                       function(num) {
+                         Assert.ok(num > 0);
+                         do_test_finished();
+                       }
+    );
   } catch (e) {
     throw new Error(`FAILED in test #${testnum} -- ${e}`);
   }
-});
+}
--- a/toolkit/components/satchel/test/unit/test_db_update_v4b.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v4b.js
@@ -1,17 +1,25 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-add_task(async function() {
-  let testnum = 0;
+var testnum = 0;
+
+var iter;
 
+function run_test() {
+  do_test_pending();
+  iter = next_test();
+  iter.next();
+}
+
+function* next_test() {
   try {
-    // ===== test init =====
+  // ===== test init =====
     let testfile = do_get_file("formhistory_v3v4.sqlite");
     let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
 
     // Cleanup from any previous tests or failures.
     let destFile = profileDir.clone();
     destFile.append("formhistory.sqlite");
     if (destFile.exists()) {
       destFile.remove(false);
@@ -22,25 +30,26 @@ add_task(async function() {
 
     // ===== 1 =====
     testnum++;
 
     destFile = profileDir.clone();
     destFile.append("formhistory.sqlite");
     let dbConnection = Services.storage.openUnsharedDatabase(destFile);
 
-    // Do something that will cause FormHistory to access and upgrade the
-    // database
-    await FormHistory.count({});
-
     // check for upgraded schema.
-    Assert.equal(CURRENT_SCHEMA, getDBVersion(destFile));
+    Assert.equal(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
     // Check that the index was added
     Assert.ok(dbConnection.tableExists("moz_deleted_formhistory"));
     dbConnection.close();
 
     // check that an entry still exists
-    Assert.ok(await promiseCountEntries("name-A", "value-A") > 0);
+    yield countEntries("name-A", "value-A",
+                       function(num) {
+                         Assert.ok(num > 0);
+                         do_test_finished();
+                       }
+    );
   } catch (e) {
     throw new Error(`FAILED in test #${testnum} -- ${e}`);
   }
-});
+}
--- a/toolkit/components/satchel/test/unit/test_db_update_v999a.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999a.js
@@ -6,17 +6,28 @@
  * This test uses a formhistory.sqlite with schema version set to 999 (a
  * future version). This exercies the code that allows using a future schema
  * version as long as the expected columns are present.
  *
  * Part A tests this when the columns do match, so the DB is used.
  * Part B tests this when the columns do *not* match, so the DB is reset.
  */
 
-add_task(async function() {
+var iter = tests();
+
+function run_test() {
+  do_test_pending();
+  iter.next();
+}
+
+function next_test() {
+  iter.next();
+}
+
+function* tests() {
   let testnum = 0;
 
   try {
     // ===== test init =====
     let testfile = do_get_file("formhistory_v999a.sqlite");
     let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
 
     // Cleanup from any previous tests or failures.
@@ -24,33 +35,38 @@ add_task(async function() {
     destFile.append("formhistory.sqlite");
     if (destFile.exists()) {
       destFile.remove(false);
     }
 
     testfile.copyTo(profileDir, "formhistory.sqlite");
     Assert.equal(999, getDBVersion(testfile));
 
+    let checkZero = function(num) { Assert.equal(num, 0); next_test(); };
+    let checkOne = function(num) { Assert.equal(num, 1); next_test(); };
+
     // ===== 1 =====
     testnum++;
     // Check for expected contents.
-    Assert.ok(await promiseCountEntries(null, null) > 0);
-    Assert.equal(1, await promiseCountEntries("name-A", "value-A"));
-    Assert.equal(1, await promiseCountEntries("name-B", "value-B"));
-    Assert.equal(1, await promiseCountEntries("name-C", "value-C1"));
-    Assert.equal(1, await promiseCountEntries("name-C", "value-C2"));
-    Assert.equal(1, await promiseCountEntries("name-E", "value-E"));
+    yield countEntries(null, null, function(num) { Assert.ok(num > 0); next_test(); });
+    yield countEntries("name-A", "value-A", checkOne);
+    yield countEntries("name-B", "value-B", checkOne);
+    yield countEntries("name-C", "value-C1", checkOne);
+    yield countEntries("name-C", "value-C2", checkOne);
+    yield countEntries("name-E", "value-E", checkOne);
 
     // check for downgraded schema.
-    Assert.equal(CURRENT_SCHEMA, getDBVersion(destFile));
+    Assert.equal(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
     // ===== 2 =====
     testnum++;
     // Exercise adding and removing a name/value pair
-    Assert.equal(0, await promiseCountEntries("name-D", "value-D"));
-    await promiseUpdateEntry("add", "name-D", "value-D");
-    Assert.equal(1, await promiseCountEntries("name-D", "value-D"));
-    await promiseUpdateEntry("remove", "name-D", "value-D");
-    Assert.equal(0, await promiseCountEntries("name-D", "value-D"));
+    yield countEntries("name-D", "value-D", checkZero);
+    yield updateEntry("add", "name-D", "value-D", next_test);
+    yield countEntries("name-D", "value-D", checkOne);
+    yield updateEntry("remove", "name-D", "value-D", next_test);
+    yield countEntries("name-D", "value-D", checkZero);
   } catch (e) {
     throw new Error(`FAILED in test #${testnum} -- ${e}`);
   }
-});
+
+  do_test_finished();
+}
--- a/toolkit/components/satchel/test/unit/test_db_update_v999b.js
+++ b/toolkit/components/satchel/test/unit/test_db_update_v999b.js
@@ -6,17 +6,28 @@
  * This test uses a formhistory.sqlite with schema version set to 999 (a
  * future version). This exercies the code that allows using a future schema
  * version as long as the expected columns are present.
  *
  * Part A tests this when the columns do match, so the DB is used.
  * Part B tests this when the columns do *not* match, so the DB is reset.
  */
 
-add_task(async function() {
+var iter = tests();
+
+function run_test() {
+  do_test_pending();
+  iter.next();
+}
+
+function next_test() {
+  iter.next();
+}
+
+function* tests() {
   let testnum = 0;
 
   try {
     // ===== test init =====
     let testfile = do_get_file("formhistory_v999b.sqlite");
     let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
 
     // Cleanup from any previous tests or failures.
@@ -28,47 +39,52 @@ add_task(async function() {
 
     let bakFile = profileDir.clone();
     bakFile.append("formhistory.sqlite.corrupt");
     if (bakFile.exists()) {
       bakFile.remove(false);
     }
 
     testfile.copyTo(profileDir, "formhistory.sqlite");
-    Assert.equal(999, getDBVersion(destFile));
+    Assert.equal(999, getDBVersion(testfile));
+
+    let checkZero = function(num) { Assert.equal(num, 0); next_test(); };
+    let checkOne = function(num) { Assert.equal(num, 1); next_test(); };
 
     // ===== 1 =====
     testnum++;
 
     // Open the DB, ensure that a backup of the corrupt DB is made.
     // DB init is done lazily so the DB shouldn't be created yet.
     Assert.ok(!bakFile.exists());
     // Doing any request to the DB should create it.
-    await promiseCountEntries("", "");
+    yield countEntries("", "", next_test);
 
     Assert.ok(bakFile.exists());
     bakFile.remove(false);
 
     // ===== 2 =====
     testnum++;
     // File should be empty
-    Assert.ok(!await promiseCountEntries(null, null));
-    Assert.equal(0, await promiseCountEntries("name-A", "value-A"));
+    yield countEntries(null, null, function(num) { Assert.ok(!num); next_test(); });
+    yield countEntries("name-A", "value-A", checkZero);
     // check for current schema.
-    Assert.equal(CURRENT_SCHEMA, getDBVersion(destFile));
+    Assert.equal(CURRENT_SCHEMA, FormHistory.schemaVersion);
 
     // ===== 3 =====
     testnum++;
     // Try adding an entry
-    await promiseUpdateEntry("add", "name-A", "value-A");
-    Assert.equal(1, await promiseCountEntries(null, null));
-    Assert.equal(1, await promiseCountEntries("name-A", "value-A"));
+    yield updateEntry("add", "name-A", "value-A", next_test);
+    yield countEntries(null, null, checkOne);
+    yield countEntries("name-A", "value-A", checkOne);
 
     // ===== 4 =====
     testnum++;
     // Try removing an entry
-    await promiseUpdateEntry("remove", "name-A", "value-A");
-    Assert.equal(0, await promiseCountEntries(null, null));
-    Assert.equal(0, await promiseCountEntries("name-A", "value-A"));
+    yield updateEntry("remove", "name-A", "value-A", next_test);
+    yield countEntries(null, null, checkZero);
+    yield countEntries("name-A", "value-A", checkZero);
   } catch (e) {
     throw new Error(`FAILED in test #${testnum} -- ${e}`);
   }
-});
+
+  do_test_finished();
+}
--- a/toolkit/components/satchel/test/unit/test_history_api.js
+++ b/toolkit/components/satchel/test/unit/test_history_api.js
@@ -55,16 +55,57 @@ function promiseUpdateEntry(op, name, va
     change.fieldname = name;
   }
   if (value !== null) {
     change.value = value;
   }
   return promiseUpdate(change);
 }
 
+function promiseUpdate(change) {
+  return new Promise((resolve, reject) => {
+    FormHistory.update(change, {
+      handleError(error) {
+        this._error = error;
+      },
+      handleCompletion(reason) {
+        if (reason) {
+          reject(this._error);
+        } else {
+          resolve();
+        }
+      },
+    });
+  });
+}
+
+function promiseSearchEntries(terms, params) {
+  return new Promise((resolve, reject) => {
+    let results = [];
+    FormHistory.search(terms, params,
+                       { handleResult: result => results.push(result),
+                         handleError(error) {
+                           do_throw("Error occurred searching form history: " + error);
+                           reject(error);
+                         },
+                         handleCompletion(reason) {
+                           if (!reason) {
+                             resolve(results);
+                           }
+                         },
+                       });
+  });
+}
+
+function promiseCountEntries(name, value, checkFn) {
+  return new Promise(resolve => {
+    countEntries(name, value, function(result) { checkFn(result); resolve(); });
+  });
+}
+
 add_task(async function() {
   let oldSupportsDeletedTable = FormHistory._supportsDeletedTable;
   FormHistory._supportsDeletedTable = true;
 
   try {
   // ===== test init =====
     let testfile = do_get_file("formhistory_apitest.sqlite");
     let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
@@ -229,18 +270,18 @@ add_task(async function() {
       // Only handle the first result
       if (results.length > 0) {
         let result = results[0];
         return [result.timesUsed, result.firstUsed, result.lastUsed, result.guid];
       }
       return undefined;
     };
 
-    let results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"],
-                                           { fieldname: "field1", value: "value1" });
+    let results = await promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                             { fieldname: "field1", value: "value1" });
     let [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
     Assert.equal(1, timesUsed);
     Assert.ok(firstUsed > 0);
     Assert.ok(lastUsed > 0);
     await promiseCountEntries(null, null, num => Assert.equal(num, 1));
 
     // ===== 11 =====
     // Add another single entry
@@ -249,86 +290,86 @@ add_task(async function() {
     await promiseCountEntries("field1", "value1", checkExists);
     await promiseCountEntries("field1", "value1b", checkExists);
     await promiseCountEntries(null, null, num => Assert.equal(num, 2));
 
     // ===== 12 =====
     // Update a single entry
     testnum++;
 
-    results = await FormHistory.search(["guid"], { fieldname: "field1", value: "value1" });
+    results = await promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1" });
     let guid = processFirstResult(results)[3];
 
     await promiseUpdate({ op: "update", guid, value: "modifiedValue" });
     await promiseCountEntries("field1", "modifiedValue", checkExists);
     await promiseCountEntries("field1", "value1", checkNotExists);
     await promiseCountEntries("field1", "value1b", checkExists);
     await promiseCountEntries(null, null, num => Assert.equal(num, 2));
 
     // ===== 13 =====
     // Add a single entry with times
     testnum++;
     await promiseUpdate({ op: "add", fieldname: "field2", value: "value2",
       timesUsed: 20, firstUsed: 100, lastUsed: 500 });
 
-    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"],
-                                       { fieldname: "field2", value: "value2" });
+    results = await promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                         { fieldname: "field2", value: "value2" });
     [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
 
     Assert.equal(20, timesUsed);
     Assert.equal(100, firstUsed);
     Assert.equal(500, lastUsed);
     await promiseCountEntries(null, null, num => Assert.equal(num, 3));
 
     // ===== 14 =====
     // Bump an entry, which updates its lastUsed field
     testnum++;
     await promiseUpdate({ op: "bump", fieldname: "field2", value: "value2",
       timesUsed: 20, firstUsed: 100, lastUsed: 500 });
-    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"],
-                                       { fieldname: "field2", value: "value2" });
+    results = await promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                         { fieldname: "field2", value: "value2" });
     [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
     Assert.equal(21, timesUsed);
     Assert.equal(100, firstUsed);
     Assert.ok(lastUsed > 500);
     await promiseCountEntries(null, null, num => Assert.equal(num, 3));
 
     // ===== 15 =====
     // Bump an entry that does not exist
     testnum++;
     await promiseUpdate({ op: "bump", fieldname: "field3", value: "value3",
       timesUsed: 10, firstUsed: 50, lastUsed: 400 });
-    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"],
-                                       { fieldname: "field3", value: "value3" });
+    results = await promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                         { fieldname: "field3", value: "value3" });
     [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
     Assert.equal(10, timesUsed);
     Assert.equal(50, firstUsed);
     Assert.equal(400, lastUsed);
     await promiseCountEntries(null, null, num => Assert.equal(num, 4));
 
     // ===== 16 =====
     // Bump an entry with a guid
     testnum++;
-    results = await FormHistory.search(["guid"], { fieldname: "field3", value: "value3" });
+    results = await promiseSearchEntries(["guid"], { fieldname: "field3", value: "value3" });
     guid = processFirstResult(results)[3];
     await promiseUpdate({ op: "bump", guid, timesUsed: 20, firstUsed: 55, lastUsed: 400 });
-    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"],
-                                       { fieldname: "field3", value: "value3" });
+    results = await promiseSearchEntries(["timesUsed", "firstUsed", "lastUsed"],
+                                         { fieldname: "field3", value: "value3" });
     [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
     Assert.equal(11, timesUsed);
     Assert.equal(50, firstUsed);
     Assert.ok(lastUsed > 400);
     await promiseCountEntries(null, null, num => Assert.equal(num, 4));
 
     // ===== 17 =====
     // Remove an entry
     testnum++;
     await countDeletedEntries(7);
 
-    results = await FormHistory.search(["guid"], { fieldname: "field1", value: "value1b" });
+    results = await promiseSearchEntries(["guid"], { fieldname: "field1", value: "value1b" });
     guid = processFirstResult(results)[3];
 
     await promiseUpdate({ op: "remove", guid});
     await promiseCountEntries("field1", "modifiedValue", checkExists);
     await promiseCountEntries("field1", "value1b", checkNotExists);
     await promiseCountEntries(null, null, num => Assert.equal(num, 3));
 
     await countDeletedEntries(8);
@@ -360,17 +401,17 @@ add_task(async function() {
       timesUsed: 5, firstUsed: 230, lastUsed: 600 },
     { op: "add", fieldname: "field6", value: "value6",
       timesUsed: 12, firstUsed: 430, lastUsed: 700 }]);
     await promiseCountEntries(null, null, num => Assert.equal(num, 4));
 
     await promiseUpdate([
       { op: "bump", fieldname: "field5", value: "value5" },
       { op: "bump", fieldname: "field6", value: "value6" }]);
-    results = await FormHistory.search(["fieldname", "timesUsed", "firstUsed", "lastUsed"], { });
+    results = await promiseSearchEntries(["fieldname", "timesUsed", "firstUsed", "lastUsed"], { });
 
     Assert.equal(6, results[2].timesUsed);
     Assert.equal(13, results[3].timesUsed);
     Assert.equal(230, results[2].firstUsed);
     Assert.equal(430, results[3].firstUsed);
     Assert.ok(results[2].lastUsed > 600);
     Assert.ok(results[3].lastUsed > 700);