Bug 562756: Use sqlite transactions and open the extensions database exclusively. r=robstrong
authorDave Townsend <dtownsend@oxymoronical.com>
Thu, 27 May 2010 15:10:09 -0700
changeset 42885 317fcd674ee701ff14a3c4ddc6faf57f1e252a1a
parent 42884 20d24f34a512a10d68e5119784bc7734e2716764
child 42886 d02a2244462f2bd80a21c451fe5e592bd9732b0c
push idunknown
push userunknown
push dateunknown
reviewersrobstrong
bugs562756
milestone1.9.3a5pre
Bug 562756: Use sqlite transactions and open the extensions database exclusively. r=robstrong
toolkit/mozapps/extensions/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -683,17 +683,17 @@ function copyRowProperties(aRow, aProper
 /**
  * A generator to synchronously return result rows from an mozIStorageStatement.
  *
  * @param  aStatement
  *         The statement to execute
  */
 function resultRows(aStatement) {
   try {
-    while (aStatement.executeStep())
+    while (stepStatement(aStatement))
       yield aStatement.row;
   }
   finally {
     aStatement.reset();
   }
 }
 
 /**
@@ -1406,18 +1406,28 @@ var XPIProvider = {
         if (newAddon.type != "theme")
           newAddon.userDisabled = aMigrateData.userDisabled;
         if ("installDate" in aMigrateData)
           newAddon.installDate = aMigrateData.installDate;
         if ("targetApplications" in aMigrateData)
           newAddon.applyCompatibilityUpdate(aMigrateData, true);
       }
 
-      // Update the database.
-      XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
+      try {
+        // Update the database.
+        XPIDatabase.addAddonMetadata(newAddon, aAddonState.descriptor);
+      }
+      catch (e) {
+        // Failing to write the add-on into the database is non-fatal, the
+        // add-on will just be unavailable until we try again in a subsequent
+        // startup
+        ERROR("Failed to add add-on " + aId + " in " + aInstallLocation.name +
+              " to database");
+        return false;
+      }
 
       // Visible bootstrapped add-ons need to have their install method called
       if (newAddon.visible) {
         visibleAddons[newAddon.id] = newAddon;
         if (!newAddon.bootstrap)
           return true;
 
         let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
@@ -1557,60 +1567,71 @@ var XPIProvider = {
     let schema = Prefs.getIntPref(PREF_DB_SCHEMA, 0);
 
     let migrateData = null;
     let cache = null;
     if (schema != DB_SCHEMA) {
       // The schema has changed so migrate data from the old schema
       migrateData = XPIDatabase.migrateData(schema);
     }
-    else {
+
+    XPIDatabase.beginTransaction();
+
+    // Catch any errors during the main startup and rollback the database changes
+    try {
       // If the database exists then the previous file cache can be trusted
       // otherwise create an empty database
       let db = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
       if (db.exists()) {
         cache = Prefs.getCharPref(PREF_INSTALL_CACHE, null);
       }
       else {
         LOG("Database is missing, recreating");
         XPIDatabase.openConnection();
         XPIDatabase.createSchema();
       }
-     }
-
-    // Load the list of bootstrapped add-ons first so processFileChanges can
-    // modify it
-    this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS,
-                                         "{}"));
-    let state = this.getInstallLocationStates();
-    if (aAppChanged || changed || cache == null ||
-        cache != JSON.stringify(state)) {
-      try {
-        changed = this.processFileChanges(state, manifests, aAppChanged,
-                                          migrateData);
-      }
-      catch (e) {
-        ERROR("Error processing file changes: " + e);
+
+      // Load the list of bootstrapped add-ons first so processFileChanges can
+      // modify it
+      this.bootstrappedAddons = JSON.parse(Prefs.getCharPref(PREF_BOOTSTRAP_ADDONS,
+                                           "{}"));
+      let state = this.getInstallLocationStates();
+      if (aAppChanged || changed || cache == null ||
+          cache != JSON.stringify(state)) {
+        try {
+          changed = this.processFileChanges(state, manifests, aAppChanged,
+                                            migrateData);
+        }
+        catch (e) {
+          ERROR("Error processing file changes: " + e);
+        }
       }
+
+      // If the application crashed before completing any pending operations then
+      // we should perform them now.
+      if (changed || Prefs.getBoolPref(PREF_PENDING_OPERATIONS)) {
+        LOG("Restart necessary");
+        XPIDatabase.updateActiveAddons();
+        XPIDatabase.commitTransaction();
+        XPIDatabase.writeAddonsList([]);
+        Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
+        Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
+                                   JSON.stringify(this.bootstrappedAddons));
+        return true;
+      }
+
+      LOG("No changes found");
+      XPIDatabase.commitTransaction();
     }
-
-    // If the application crashed before completing any pending operations then
-    // we should perform them now.
-    if (changed || Prefs.getBoolPref(PREF_PENDING_OPERATIONS)) {
-      LOG("Restart necessary");
-      XPIDatabase.updateActiveAddons();
-      XPIDatabase.writeAddonsList([]);
-      Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, false);
-      Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
-                                 JSON.stringify(this.bootstrappedAddons));
-      return true;
+    catch (e) {
+      ERROR("Error during startup file checks, rolling back any database " +
+            "changes: " + e);
+      XPIDatabase.rollbackTransaction();
     }
 
-    LOG("No changes found");
-
     // Check that the add-ons list still exists
     let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
                                        true);
     if (!addonsList.exists()) {
       LOG("Add-ons list is missing, recreating");
       XPIDatabase.writeAddonsList([]);
       Services.prefs.setCharPref(PREF_BOOTSTRAP_ADDONS,
                                  JSON.stringify(this.bootstrappedAddons));
@@ -2289,19 +2310,72 @@ var XPIProvider = {
 };
 
 const FIELDS_ADDON = "internal_id, id, location, version, type, internalName, " +
                      "updateURL, updateKey, optionsURL, aboutURL, iconURL, " +
                      "defaultLocale, visible, active, userDisabled, appDisabled, " +
                      "pendingUninstall, descriptor, installDate, updateDate, " +
                      "applyBackgroundUpdates, bootstrap";
 
-// A helper function to simply log any errors that occur during async statements.
+/**
+ * A helper function to log an SQL error.
+ *
+ * @param  aError
+ *         The storage error code associated with the error
+ * @param  aErrorString
+ *         An error message
+ */
+function logSQLError(aError, aErrorString) {
+  ERROR("SQL error " + aError + ": " + aErrorString);
+}
+
+/**
+ * A helper function to log any errors that occur during async statements.
+ *
+ * @param  aError
+ *         A mozIStorageError to log
+ */
 function asyncErrorLogger(aError) {
-  ERROR("SQL error " + aError.result + ": " + aError.message);
+  logSQLError(aError.result, aError.message);
+}
+
+/**
+ * A helper function to execute a statement synchronously and log any error
+ * that occurs.
+ *
+ * @param  aStatement
+ *         A mozIStorageStatement to execute
+ */
+function executeStatement(aStatement) {
+  try {
+    aStatement.execute();
+  }
+  catch (e) {
+    logSQLError(XPIDatabase.connection.lastError,
+                XPIDatabase.connection.lastErrorString);
+    throw e;
+  }
+}
+
+/**
+ * A helper function to step a statement synchronously and log any error that
+ * occurs.
+ *
+ * @param  aStatement
+ *         A mozIStorageStatement to execute
+ */
+function stepStatement(aStatement) {
+  try {
+    return aStatement.executeStep();
+  }
+  catch (e) {
+    logSQLError(XPIDatabase.connection.lastError,
+                XPIDatabase.connection.lastErrorString);
+    throw e;
+  }
 }
 
 /**
  * A mozIStorageStatementCallback that will asynchronously build DBAddonInternal
  * instances from the results it receives. Once the statement has completed
  * executing and all of the metadata for all of the add-ons has been retrieved
  * they will be passed as an array to aCallback.
  *
@@ -2344,16 +2418,18 @@ AsyncAddonListCallback.prototype = {
 var XPIDatabase = {
   // true if the database connection has been opened
   initialized: false,
   // A cache of statements that are used and need to be finalized on shutdown
   statementCache: {},
   // A cache of weak referenced DBAddonInternals so we can reuse objects where
   // possible
   addonCache: [],
+  // The nested transaction count
+  transactionCount: 0,
 
   // The statements used by the database
   statements: {
     _getDefaultLocale: "SELECT id, name, description, creator, homepageURL " +
                        "FROM locale WHERE id=:id",
     _getLocales: "SELECT addon_locale.locale, locale.id, locale.name, " +
                  "locale.description, locale.creator, locale.homepageURL " +
                  "FROM addon_locale JOIN locale ON " +
@@ -2415,28 +2491,86 @@ var XPIDatabase = {
     setAddonProperties: "UPDATE addon SET userDisabled=:userDisabled, " +
                         "appDisabled=:appDisabled, " +
                         "pendingUninstall=:pendingUninstall, " +
                         "applyBackgroundUpdates=:applyBackgroundUpdates WHERE " +
                         "internal_id=:internal_id",
     updateTargetApplications: "UPDATE targetApplication SET " +
                               "minVersion=:minVersion, maxVersion=:maxVersion " +
                               "WHERE addon_internal_id=:internal_id AND id=:id",
+
+    createSavepoint: "SAVEPOINT 'default'",
+    releaseSavepoint: "RELEASE SAVEPOINT 'default'",
+    rollbackSavepoint: "ROLLBACK TO SAVEPOINT 'default'"
+  },
+
+  /**
+   * Begins a new transaction in the database. Transactions may be nested. Data
+   * written by an inner transaction may be rolled back on its own. Rolling back
+   * an outer transaction will rollback all the changes made by inner
+   * transactions even if they were committed. No data is written to the disk
+   * until the outermost transaction is committed. Transactions can be started
+   * even when the database is not yet open in which case they will be started
+   * when the database is first opened.
+   */
+  beginTransaction: function XPIDB_beginTransaction() {
+    if (this.initialized)
+      this.getStatement("createSavepoint").execute();
+    this.transactionCount++;
+  },
+
+  /**
+   * Commits the most recent transaction. The data may still be rolled back if
+   * an outer transaction is rolled back.
+   */
+  commitTransaction: function XPIDB_commitTransaction() {
+    if (this.transactionCount == 0) {
+      ERROR("Attempt to commit one transaction too many.");
+      return;
+    }
+
+    if (this.initialized)
+      this.getStatement("releaseSavepoint").execute();
+    this.transactionCount--;
+  },
+
+  /**
+   * Rolls back the most recent transaction. The database will return to its
+   * state when the transaction was started.
+   */
+  rollbackTransaction: function XPIDB_rollbackTransaction() {
+    if (this.transactionCount == 0) {
+      ERROR("Attempt to rollback one transaction too many.");
+      return;
+    }
+
+    if (this.initialized) {
+      this.getStatement("rollbackSavepoint").execute();
+      this.getStatement("releaseSavepoint").execute();
+    }
+    this.transactionCount--;
   },
 
   /**
    * Opens a new connection to the database file.
    *
    * @return the mozIStorageConnection for the database
    */
   openConnection: function XPIDB_openConnection() {
     this.initialized = true;
     let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
     delete this.connection;
-    return this.connection = Services.storage.openDatabase(dbfile);
+    this.connection = Services.storage.openUnsharedDatabase(dbfile);
+    this.connection.executeSimpleSQL("PRAGMA synchronous = FULL");
+    this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+
+    // Begin any pending transactions
+    for (let i = 0; i < this.transactionCount; i++)
+      this.connection.executeSimpleSQL("SAVEPOINT 'default'");
+    return this.connection;
   },
 
   /**
    * A lazy getter for the database connection.
    */
   get connection() {
     return this.openConnection();
   },
@@ -2514,37 +2648,43 @@ var XPIDatabase = {
       catch (e) {
         // An error here means the schema is too different to read
         ERROR("Error migrating data: " + e);
       }
       finally {
         stmt.finalize();
       }
       this.connection.close();
+      this.initialized = false;
     }
 
-    // Create a clean database to work with
+    // Delete any existing database file
     let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
     if (dbfile.exists())
       dbfile.remove(true);
-    this.openConnection();
-    this.createSchema();
 
     return migrateData;
   },
 
   /**
    * Shuts down the database connection and releases all cached objects.
    */
   shutdown: function XPIDB_shutdown() {
     if (this.initialized) {
       for each (let stmt in this.statementCache)
         stmt.finalize();
       this.statementCache = {};
       this.addonCache = [];
+
+      if (this.transactionCount > 0) {
+        ERROR(this.transactionCount + " outstanding transactions, rolling back.");
+        while (this.transactionCount > 0)
+          this.rollbackTransaction();
+      }
+
       this.connection.asyncClose();
       this.initialized = false;
       delete this.connection;
 
       // Re-create the connection smart getter to allow the database to be
       // re-loaded during testing.
       this.__defineGetter__("connection", function() {
         return this.openConnection();
@@ -2578,59 +2718,71 @@ var XPIDatabase = {
     }
   },
 
   /**
    * Creates the schema in the database.
    */
   createSchema: function XPIDB_createSchema() {
     LOG("Creating database schema");
-    this.connection.createTable("addon",
-                                "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
-                                "id TEXT, location TEXT, version TEXT, " +
-                                "type TEXT, internalName TEXT, updateURL TEXT, " +
-                                "updateKey TEXT, optionsURL TEXT, aboutURL TEXT, " +
-                                "iconURL TEXT, defaultLocale INTEGER, " +
-                                "visible INTEGER, active INTEGER, " +
-                                "userDisabled INTEGER, appDisabled INTEGER, " +
-                                "pendingUninstall INTEGER, descriptor TEXT, " +
-                                "installDate INTEGER, updateDate INTEGER, " +
-                                "applyBackgroundUpdates INTEGER, " +
-                                "bootstrap INTEGER, UNIQUE (id, location)");
-    this.connection.createTable("targetApplication",
-                                "addon_internal_id INTEGER, " +
-                                "id TEXT, minVersion TEXT, maxVersion TEXT, " +
-                                "UNIQUE (addon_internal_id, id)");
-    this.connection.createTable("addon_locale",
-                                "addon_internal_id INTEGER, "+
-                                "locale TEXT, locale_id INTEGER, " +
-                                "UNIQUE (addon_internal_id, locale)");
-    this.connection.createTable("locale",
-                                "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
-                                "name TEXT, description TEXT, creator TEXT, " +
-                                "homepageURL TEXT");
-    this.connection.createTable("locale_strings",
-                                "locale_id INTEGER, type TEXT, value TEXT");
-    this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
-      "ON addon BEGIN " +
-      "DELETE FROM targetApplication WHERE addon_internal_id=old.internal_id; " +
-      "DELETE FROM addon_locale WHERE addon_internal_id=old.internal_id; " +
-      "DELETE FROM locale WHERE id=old.defaultLocale; " +
-      "END");
-    this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon_locale AFTER " +
-      "DELETE ON addon_locale WHEN NOT EXISTS " +
-      "(SELECT * FROM addon_locale WHERE locale_id=old.locale_id) BEGIN " +
-      "DELETE FROM locale WHERE id=old.locale_id; " +
-      "END");
-    this.connection.executeSimpleSQL("CREATE TRIGGER delete_locale AFTER " +
-      "DELETE ON locale BEGIN " +
-      "DELETE FROM locale_strings WHERE locale_id=old.id; " +
-      "END");
-    this.connection.schemaVersion = DB_SCHEMA;
-    Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+    this.beginTransaction();
+
+    // Any errors in here should rollback the transaction
+    try {
+      this.connection.createTable("addon",
+                                  "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                                  "id TEXT, location TEXT, version TEXT, " +
+                                  "type TEXT, internalName TEXT, updateURL TEXT, " +
+                                  "updateKey TEXT, optionsURL TEXT, aboutURL TEXT, " +
+                                  "iconURL TEXT, defaultLocale INTEGER, " +
+                                  "visible INTEGER, active INTEGER, " +
+                                  "userDisabled INTEGER, appDisabled INTEGER, " +
+                                  "pendingUninstall INTEGER, descriptor TEXT, " +
+                                  "installDate INTEGER, updateDate INTEGER, " +
+                                  "applyBackgroundUpdates INTEGER, " +
+                                  "bootstrap INTEGER, UNIQUE (id, location)");
+      this.connection.createTable("targetApplication",
+                                  "addon_internal_id INTEGER, " +
+                                  "id TEXT, minVersion TEXT, maxVersion TEXT, " +
+                                  "UNIQUE (addon_internal_id, id)");
+      this.connection.createTable("addon_locale",
+                                  "addon_internal_id INTEGER, "+
+                                  "locale TEXT, locale_id INTEGER, " +
+                                  "UNIQUE (addon_internal_id, locale)");
+      this.connection.createTable("locale",
+                                  "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+                                  "name TEXT, description TEXT, creator TEXT, " +
+                                  "homepageURL TEXT");
+      this.connection.createTable("locale_strings",
+                                  "locale_id INTEGER, type TEXT, value TEXT");
+      this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
+        "ON addon BEGIN " +
+        "DELETE FROM targetApplication WHERE addon_internal_id=old.internal_id; " +
+        "DELETE FROM addon_locale WHERE addon_internal_id=old.internal_id; " +
+        "DELETE FROM locale WHERE id=old.defaultLocale; " +
+        "END");
+      this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon_locale AFTER " +
+        "DELETE ON addon_locale WHEN NOT EXISTS " +
+        "(SELECT * FROM addon_locale WHERE locale_id=old.locale_id) BEGIN " +
+        "DELETE FROM locale WHERE id=old.locale_id; " +
+        "END");
+      this.connection.executeSimpleSQL("CREATE TRIGGER delete_locale AFTER " +
+        "DELETE ON locale BEGIN " +
+        "DELETE FROM locale_strings WHERE locale_id=old.id; " +
+        "END");
+      this.connection.schemaVersion = DB_SCHEMA;
+      Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
+      this.commitTransaction();
+    }
+    catch (e) {
+      ERROR("Failed to create database schema");
+      logSQLError(this.connection.lastError, this.connection.lastErrorString);
+      this.rollbackTransaction();
+      throw e;
+    }
   },
 
   /**
    * Synchronously reads the multi-value locale strings for a locale
    *
    * @param  aLocale
    *         The locale object to read into
    */
@@ -2678,17 +2830,17 @@ var XPIDatabase = {
    *         The DBAddonInternal to read the default locale for
    * @return the default locale for the add-on
    * @throws if the database does not contain the default locale information
    */
   _getDefaultLocale: function XPIDB__getDefaultLocale(aAddon) {
     let stmt = this.getStatement("_getDefaultLocale");
 
     stmt.params.id = aAddon._defaultLocale;
-    if (!stmt.executeStep())
+    if (!stepStatement(stmt))
       throw new Error("Missing default locale for " + aAddon.id);
     let locale = copyProperties(stmt.row, PROP_LOCALE_SINGLE);
     locale.id = aAddon._defaultLocale;
     stmt.reset();
     this._readLocaleStrings(locale);
     return locale;
   },
 
@@ -3080,151 +3232,181 @@ var XPIDatabase = {
    * Synchronously adds an AddonInternal's metadata to the database.
    *
    * @param  aAddon
    *         AddonInternal to add
    * @param  aDescriptor
    *         The file descriptor of the add-on's directory
    */
   addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
-    let localestmt = this.getStatement("addAddonMetadata_locale");
-    let stringstmt = this.getStatement("addAddonMetadata_strings");
-
-    function insertLocale(aLocale) {
-      copyProperties(aLocale, PROP_LOCALE_SINGLE, localestmt.params);
-      localestmt.execute();
-      let row = XPIDatabase.connection.lastInsertRowID;
-
-      PROP_LOCALE_MULTI.forEach(function(aProp) {
-        aLocale[aProp].forEach(function(aStr) {
-          stringstmt.params.locale = row;
-          stringstmt.params.type = aProp;
-          stringstmt.params.value = aStr;
-          stringstmt.execute();
+    this.beginTransaction();
+
+    // Any errors in here should rollback the transaction
+    try {
+      let localestmt = this.getStatement("addAddonMetadata_locale");
+      let stringstmt = this.getStatement("addAddonMetadata_strings");
+
+      function insertLocale(aLocale) {
+        copyProperties(aLocale, PROP_LOCALE_SINGLE, localestmt.params);
+        executeStatement(localestmt);
+        let row = XPIDatabase.connection.lastInsertRowID;
+
+        PROP_LOCALE_MULTI.forEach(function(aProp) {
+          aLocale[aProp].forEach(function(aStr) {
+            stringstmt.params.locale = row;
+            stringstmt.params.type = aProp;
+            stringstmt.params.value = aStr;
+            executeStatement(stringstmt);
+          });
+        });
+        return row;
+      }
+
+      aAddon.active = (aAddon.visible && !aAddon.userDisabled &&
+                       !aAddon.appDisabled);
+
+      if (aAddon.visible) {
+        let stmt = this.getStatement("clearVisibleAddons");
+        stmt.params.id = aAddon.id;
+        executeStatement(stmt);
+      }
+
+      let stmt = this.getStatement("addAddonMetadata_addon");
+
+      stmt.params.locale = insertLocale(aAddon.defaultLocale);
+      stmt.params.location = aAddon._installLocation.name;
+      stmt.params.descriptor = aDescriptor;
+      stmt.params.installDate = aAddon.installDate;
+      stmt.params.updateDate = aAddon.updateDate;
+      copyProperties(aAddon, PROP_METADATA, stmt.params);
+      ["visible", "userDisabled", "appDisabled", "applyBackgroundUpdates",
+       "bootstrap"].forEach(function(aProp) {
+        stmt.params[aProp] = aAddon[aProp] ? 1 : 0;
+      });
+      stmt.params.active = aAddon.active ? 1 : 0;
+      executeStatement(stmt);
+      let internal_id = this.connection.lastInsertRowID;
+
+      stmt = this.getStatement("addAddonMetadata_addon_locale");
+      aAddon.locales.forEach(function(aLocale) {
+        let id = insertLocale(aLocale);
+        aLocale.locales.forEach(function(aName) {
+          stmt.params.internal_id = internal_id;
+          stmt.params.name = aName;
+          stmt.params.locale = insertLocale(aLocale);
+          executeStatement(stmt);
         });
       });
-      return row;
-    }
-
-    aAddon.active = (aAddon.visible && !aAddon.userDisabled &&
-                     !aAddon.appDisabled);
-
-    if (aAddon.visible) {
-      let stmt = this.getStatement("clearVisibleAddons");
-      stmt.params.id = aAddon.id;
-      stmt.execute();
+
+      stmt = this.getStatement("addAddonMetadata_targetApplication");
+
+      aAddon.targetApplications.forEach(function(aApp) {
+        stmt.params.internal_id = internal_id;
+        stmt.params.id = aApp.id;
+        stmt.params.minVersion = aApp.minVersion;
+        stmt.params.maxVersion = aApp.maxVersion;
+        executeStatement(stmt);
+      });
+      this.commitTransaction();
     }
-
-    let stmt = this.getStatement("addAddonMetadata_addon");
-
-    stmt.params.locale = insertLocale(aAddon.defaultLocale);
-    stmt.params.location = aAddon._installLocation.name;
-    stmt.params.descriptor = aDescriptor;
-    stmt.params.installDate = aAddon.installDate;
-    stmt.params.updateDate = aAddon.updateDate;
-    copyProperties(aAddon, PROP_METADATA, stmt.params);
-    ["visible", "userDisabled", "appDisabled", "applyBackgroundUpdates",
-     "bootstrap"].forEach(function(aProp) {
-      stmt.params[aProp] = aAddon[aProp] ? 1 : 0;
-    });
-    stmt.params.active = aAddon.active ? 1 : 0;
-    stmt.execute();
-    let internal_id = this.connection.lastInsertRowID;
-
-    stmt = this.getStatement("addAddonMetadata_addon_locale");
-    aAddon.locales.forEach(function(aLocale) {
-      let id = insertLocale(aLocale);
-      aLocale.locales.forEach(function(aName) {
-        stmt.params.internal_id = internal_id;
-        stmt.params.name = aName;
-        stmt.params.locale = insertLocale(aLocale);
-        stmt.execute();
-      });
-    });
-
-    stmt = this.getStatement("addAddonMetadata_targetApplication");
-
-    aAddon.targetApplications.forEach(function(aApp) {
-      stmt.params.internal_id = internal_id;
-      stmt.params.id = aApp.id;
-      stmt.params.minVersion = aApp.minVersion;
-      stmt.params.maxVersion = aApp.maxVersion;
-      stmt.execute();
-    });
+    catch (e) {
+      this.rollbackTransaction();
+      throw e;
+    }
   },
 
   /**
    * Synchronously updates an add-ons metadata in the database. Currently just
    * removes and recreates.
    *
    * @param  aOldAddon
    *         The DBAddonInternal to be replaced
    * @param  aNewAddon
    *         The new AddonInternal to add
    * @param  aDescriptor
    *         The file descriptor of the add-on's directory
    */
   updateAddonMetadata: function XPIDB_updateAddonMetadata(aOldAddon, aNewAddon,
                                                           aDescriptor) {
-    this.removeAddonMetadata(aOldAddon);
-    aNewAddon.userDisabled = aOldAddon.userDisabled;
-    aNewAddon.installDate = aOldAddon.installDate;
-    aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
-    this.addAddonMetadata(aNewAddon, aDescriptor);
+    this.beginTransaction();
+
+    // Any errors in here should rollback the transaction
+    try {
+      this.removeAddonMetadata(aOldAddon);
+      aNewAddon.userDisabled = aOldAddon.userDisabled;
+      aNewAddon.installDate = aOldAddon.installDate;
+      aNewAddon.applyBackgroundUpdates = aOldAddon.applyBackgroundUpdates;
+      this.addAddonMetadata(aNewAddon, aDescriptor);
+      this.commitTransaction();
+    }
+    catch (e) {
+      this.rollbackTransaction();
+      throw e;
+    }
   },
 
   /**
    * Synchronously updates the target application entries for an add-on.
    *
    * @param  aAddon
    *         The DBAddonInternal being updated
    * @param  aTargets
    *         The array of target applications to update
    */
   updateTargetApplications: function XPIDB_updateTargetApplications(aAddon,
                                                                     aTargets) {
-    let stmt = this.getStatement("updateTargetApplications");
-    aTargets.forEach(function(aTarget) {
-      stmt.params.internal_id = aAddon._internal_id;
-      stmt.params.id = aTarget.id;
-      stmt.params.minVersion = aTarget.minVersion;
-      stmt.params.maxVersion = aTarget.maxVersion;
-      stmt.execute();
-    });
+    this.beginTransaction();
+
+    // Any errors in here should rollback the transaction
+    try {
+      let stmt = this.getStatement("updateTargetApplications");
+      aTargets.forEach(function(aTarget) {
+        stmt.params.internal_id = aAddon._internal_id;
+        stmt.params.id = aTarget.id;
+        stmt.params.minVersion = aTarget.minVersion;
+        stmt.params.maxVersion = aTarget.maxVersion;
+        executeStatement(stmt);
+      });
+      this.commitTransaction();
+    }
+    catch (e) {
+      this.rollbackTransaction();
+      throw e;
+    }
   },
 
   /**
    * Synchronously removes an add-on from the database.
    *
    * @param  aAddon
    *         The DBAddonInternal being removed
    */
   removeAddonMetadata: function XPIDB_removeAddonMetadata(aAddon) {
     let stmt = this.getStatement("removeAddonMetadata");
     stmt.params.internal_id = aAddon._internal_id;
-    stmt.execute();
+    executeStatement(stmt);
   },
 
   /**
    * Synchronously marks a DBAddonInternal as visible marking all other
    * instances with the same ID as not visible.
    *
    * @param  aAddon
    *         The DBAddonInternal to make visible
    * @param  callback
    *         A callback to pass the DBAddonInternal to
    */
   makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) {
     let stmt = this.getStatement("clearVisibleAddons");
     stmt.params.id = aAddon.id;
-    stmt.execute();
+    executeStatement(stmt);
 
     stmt = this.getStatement("makeAddonVisible");
     stmt.params.internal_id = aAddon._internal_id;
-    stmt.execute();
+    executeStatement(stmt);
 
     aAddon.visible = true;
   },
 
   /**
    * Synchronously sets properties for an add-on.
    *
    * @param  aAddon
@@ -3246,41 +3428,41 @@ var XPIDatabase = {
         stmt.params[aProp] = convertBoolean(aProperties[aProp]);
         aAddon[aProp] = aProperties[aProp];
       }
       else {
         stmt.params[aProp] = convertBoolean(aAddon[aProp]);
       }
     });
 
-    stmt.execute();
+    executeStatement(stmt);
   },
 
   /**
    * Synchronously pdates an add-on's active flag in the database.
    *
    * @param  aAddon
    *         The DBAddonInternal to update
    */
   updateAddonActive: function XPIDB_updateAddonActive(aAddon) {
     LOG("Updating add-on state");
 
     stmt = this.getStatement("updateAddonActive");
     stmt.params.internal_id = aAddon._internal_id;
     stmt.params.active = aAddon.active ? 1 : 0;
-    stmt.execute();
+    executeStatement(stmt);
   },
 
   /**
    * Synchronously calculates and updates all the active flags in the database.
    */
   updateActiveAddons: function XPIDB_updateActiveAddons() {
     LOG("Updating add-on states");
     let stmt = this.getStatement("setActiveAddons");
-    stmt.execute();
+    executeStatement(stmt);
   },
 
   /**
    * Writes out the XPI add-ons list for the platform to read.
    *
    * @param  aPendingUpdateIDs
    *         An array of IDs of add-ons that are pending update and so shouldn't
    *         be included in the add-ons list.
@@ -4427,17 +4609,25 @@ DBAddonInternal.prototype = {
         if (aTargetApp.id == aUpdateTarget.id && (aSyncCompatibility ||
             Services.vc.compare(aTargetApp.maxVersion, aUpdateTarget.maxVersion) < 0)) {
           aTargetApp.minVersion = aUpdateTarget.minVersion;
           aTargetApp.maxVersion = aUpdateTarget.maxVersion;
           changes.push(aUpdateTarget);
         }
       });
     });
-    XPIDatabase.updateTargetApplications(this, changes);
+    try {
+      XPIDatabase.updateTargetApplications(this, changes);
+    }
+    catch (e) {
+      // A failure just means that we discard the compatibility update
+      ERROR("Failed to update target application info in the database for " +
+            "add-on " + this.id);
+      return;
+    }
     XPIProvider.updateAddonDisabledState(this);
   }
 }
 
 DBAddonInternal.prototype.__proto__ = AddonInternal.prototype;
 
 /**
  * Creates an AddonWrapper for an AddonInternal.
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -199,16 +199,55 @@ function shutdownManager() {
   gInternalManager.observe(null, "xpcom-shutdown", null);
   gInternalManager = null;
 
   // Load the add-ons list as it was after application shutdown
   loadAddonsList(false);
 
   // Clear any crash report annotations
   gAppInfo.annotations = {};
+
+  let dbfile = gProfD.clone();
+  dbfile.append("extensions.sqlite");
+
+  // If there is no database then it cannot be locked.
+  if (!dbfile.exists())
+    return;
+
+  let thr = AM_Cc["@mozilla.org/thread-manager;1"].
+            getService(AM_Ci.nsIThreadManager).
+            mainThread;
+
+  // Wait until we can open a connection to the database
+  let db = null;
+  while (!db) {
+    // Poll for database
+    try {
+      db = Services.storage.openUnsharedDatabase(dbfile);
+    }
+    catch (e) {
+      if (thr.hasPendingEvents())
+        thr.processNextEvent(false);
+    }
+  }
+
+  // Wait until we can write to the database
+  while (db) {
+    // Poll for write access
+    try {
+      db.executeSimpleSQL("PRAGMA user_version = 1");
+      db.executeSimpleSQL("PRAGMA user_version = 0");
+      db.close();
+      db = null;
+    }
+    catch (e) {
+      if (thr.hasPendingEvents())
+        thr.processNextEvent(false);
+    }
+  }
 }
 
 function loadAddonsList(aAppChanged) {
   function readDirectories(aSection) {
     var dirs = [];
     var keys = parser.getKeys(aSection);
     while (keys.hasMore()) {
       let descriptor = parser.getString(aSection, keys.getNext());
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js
@@ -4,42 +4,65 @@
 
 // This verifies that deleting the database from the profile doesn't break
 // anything
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
 function run_test() {
-  do_test_pending();
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   var dest = profileDir.clone();
   dest.append("addon1@tests.mozilla.org");
   writeInstallRDFToDir({
     id: "addon1@tests.mozilla.org",
     version: "1.0",
     updateURL: "http://localhost:4444/data/test_update.rdf",
     targetApplications: [{
       id: "xpcshell@tests.mozilla.org",
       minVersion: "1",
       maxVersion: "1"
     }],
     name: "Test Addon 1",
   }, dest);
 
-  // Make it look like the detabase was created previously
-  Services.prefs.setIntPref("extensions.databaseSchema", 1);
+  startupManager(1);
+
+  do_test_pending();
+
+  run_test_1();
+}
+
+function end_test() {
+  do_test_finished();
+}
 
-  startupManager(1);
+function run_test_1() {
+  AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+    do_check_neq(a1, null);
+    do_check_eq(a1.version, "1.0");
+
+    shutdownManager();
+
+    let db = gProfD.clone();
+    db.append("extensions.sqlite");
+    db.remove(true);
+
+    check_test_1();
+  });
+}
+
+function check_test_1() {
+  startupManager(1, false);
 
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
     do_check_neq(a1, null);
     do_check_eq(a1.version, "1.0");
 
     let db = gProfD.clone();
     db.append("extensions.sqlite");
     do_check_true(db.exists());
     do_check_true(db.fileSize > 0);
 
-    do_test_finished();
+    end_test();
   });
 }