--- a/toolkit/components/contentprefs/ContentPrefService2.js
+++ b/toolkit/components/contentprefs/ContentPrefService2.js
@@ -1,42 +1,40 @@
/* 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/. */
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/ContentPrefUtils.jsm");
ChromeUtils.import("resource://gre/modules/ContentPrefStore.jsm");
+ChromeUtils.defineModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
const CACHE_MAX_GROUP_ENTRIES = 100;
const GROUP_CLAUSE = `
SELECT id
FROM groups
WHERE name = :group OR
(:includeSubdomains AND name LIKE :pattern ESCAPE '/')
`;
function ContentPrefService2() {
if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
return ChromeUtils.import("resource://gre/modules/ContentPrefServiceChild.jsm")
.ContentPrefServiceChild;
}
- // If this throws an exception, it causes the getService call to fail,
- // but the next time a consumer tries to retrieve the service, we'll try
- // to initialize the database again, which might work if the failure
- // was due to a temporary condition (like being out of disk space).
- this._dbInit();
-
Services.obs.addObserver(this, "last-pb-context-exited");
// Observe shutdown so we can shut down the database connection.
- Services.obs.addObserver(this, "xpcom-shutdown");
+ Services.obs.addObserver(this, "profile-before-change");
}
const cache = new ContentPrefStore();
cache.set = function CPS_cache_set(group, name, val) {
Object.getPrototypeOf(this).set.apply(this, arguments);
let groupCount = this._groups.size;
if (groupCount >= CACHE_MAX_GROUP_ENTRIES) {
// Clean half of the entries
@@ -53,41 +51,54 @@ const privModeStorage = new ContentPrefS
ContentPrefService2.prototype = {
// XPCOM Plumbing
classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"),
// Destruction
- _destroy: function ContentPrefService__destroy() {
- Services.obs.removeObserver(this, "xpcom-shutdown");
+ _destroy: function CPS2__destroy() {
+ Services.obs.removeObserver(this, "profile-before-change");
Services.obs.removeObserver(this, "last-pb-context-exited");
- this.destroy();
-
- this._dbConnection.asyncClose(() => {
- Services.obs.notifyObservers(null, "content-prefs-db-closed");
- });
-
// Delete references to XPCOM components to make sure we don't leak them
// (although we haven't observed leakage in tests). Also delete references
// in _observers and _genericObservers to avoid cycles with those that
// refer to us and don't remove themselves from those observer pools.
delete this._observers;
delete this._genericObservers;
delete this.__grouper;
},
// in-memory cache and private-browsing stores
_cache: cache,
_pbStore: privModeStorage,
+ _connPromise: null,
+
+ get conn() {
+ if (this._connPromise) {
+ return this._connPromise;
+ }
+
+ return this._connPromise = new Promise(async (resolve, reject) => {
+ let conn;
+ try {
+ conn = await this._getConnection();
+ } catch (e) {
+ this.log("Failed to establish database connection: " + e);
+ reject(e);
+ }
+ resolve(conn);
+ });
+ },
+
// nsIContentPrefService
getByName: function CPS2_getByName(name, context, callback) {
checkNameArg(name);
checkCallbackArg(callback, true);
// Some prefs may be in both the database and the private browsing store.
// Notify the caller of such prefs only once, using the values from private
@@ -114,32 +125,32 @@ ContentPrefService2.prototype = {
SELECT NULL AS grp, prefs.value AS value
FROM prefs
JOIN settings ON settings.id = prefs.settingID
WHERE settings.name = :name AND prefs.groupID ISNULL
`);
stmt2.params.name = name;
this._execStmts([stmt1, stmt2], {
- onRow: function onRow(row) {
+ onRow: row => {
let grp = row.getResultByName("grp");
let val = row.getResultByName("value");
this._cache.set(grp, name, val);
if (!pbPrefs.has(grp, name))
cbHandleResult(callback, new ContentPref(grp, name, val));
},
- onDone: function onDone(reason, ok, gotRow) {
+ onDone: (reason, ok, gotRow) => {
if (ok) {
for (let [pbGroup, pbName, pbVal] of pbPrefs) {
cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
}
}
cbHandleCompletion(callback, reason);
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
getByDomainAndName: function CPS2_getByDomainAndName(group, name, context,
callback) {
checkGroupArg(group);
@@ -169,40 +180,42 @@ ContentPrefService2.prototype = {
if (context && context.usePrivateBrowsing) {
for (let [sgroup, val] of
this._pbStore.match(group, name, includeSubdomains)) {
pbPrefs.set(sgroup, name, val);
}
}
this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], {
- onRow: function onRow(row) {
+ onRow: row => {
let grp = row.getResultByName("grp");
let val = row.getResultByName("value");
this._cache.set(grp, name, val);
if (!pbPrefs.has(group, name))
cbHandleResult(callback, new ContentPref(grp, name, val));
},
- onDone: function onDone(reason, ok, gotRow) {
+ onDone: (reason, ok, gotRow) => {
if (ok) {
if (!gotRow)
this._cache.set(group, name, undefined);
for (let [pbGroup, pbName, pbVal] of pbPrefs) {
cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal));
}
}
cbHandleCompletion(callback, reason);
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
- _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) {
+ _commonGetStmt: function CPS2__commonGetStmt(group,
+ name,
+ includeSubdomains) {
let stmt = group ?
this._stmtWithGroupClause(group, includeSubdomains, `
SELECT groups.name AS grp, prefs.value AS value
FROM prefs
JOIN settings ON settings.id = prefs.settingID
JOIN groups ON groups.id = prefs.groupID
WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE})
`) :
@@ -214,20 +227,21 @@ ContentPrefService2.prototype = {
`);
stmt.params.name = name;
return stmt;
},
_stmtWithGroupClause: function CPS2__stmtWithGroupClause(group,
includeSubdomains,
sql) {
- let stmt = this._stmt(sql);
+ let stmt = this._stmt(sql, false);
stmt.params.group = group;
stmt.params.includeSubdomains = includeSubdomains || false;
- stmt.params.pattern = "%." + stmt.escapeStringForLIKE(group, "/");
+ stmt.params.pattern = "%." + (group == null ? null :
+ group.replace(/\/|%|_/g, "/$&"));
return stmt;
},
getCachedByDomainAndName: function CPS2_getCachedByDomainAndName(group,
name,
context) {
checkGroupArg(group);
let prefs = this._getCached(group, name, false, context);
@@ -354,24 +368,24 @@ ContentPrefService2.prototype = {
`);
}
stmt.params.name = name;
stmt.params.value = value;
stmt.params.now = Date.now() / 1000;
stmts.push(stmt);
this._execStmts(stmts, {
- onDone: function onDone(reason, ok) {
+ onDone: (reason, ok) => {
if (ok)
this._cache.setWithCast(group, name, value);
cbHandleCompletion(callback, reason);
if (ok)
this._notifyPrefSet(group, name, value, context && context.usePrivateBrowsing);
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
removeByDomainAndName: function CPS2_removeByDomainAndName(group, name,
context,
callback) {
@@ -420,22 +434,22 @@ ContentPrefService2.prototype = {
stmts.push(stmt);
stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
let prefs = new ContentPrefStore();
let isPrivate = context && context.usePrivateBrowsing;
this._execStmts(stmts, {
- onRow: function onRow(row) {
+ onRow: row => {
let grp = row.getResultByName("grp");
prefs.set(grp, name, undefined);
this._cache.set(grp, name, undefined);
},
- onDone: function onDone(reason, ok) {
+ onDone: (reason, ok) => {
if (ok) {
this._cache.set(group, name, undefined);
if (isPrivate) {
for (let [sgroup, ] of
this._pbStore.match(group, name, includeSubdomains)) {
prefs.set(sgroup, name, undefined);
this._pbStore.remove(sgroup, name);
}
@@ -443,17 +457,17 @@ ContentPrefService2.prototype = {
}
cbHandleCompletion(callback, reason);
if (ok) {
for (let [sgroup, , ] of prefs) {
this._notifyPrefRemoved(sgroup, name, isPrivate);
}
}
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
// Deletes settings and groups that are no longer used.
_settingsAndGroupsCleanupStmts() {
// The NOTNULL term in the subquery of the second statment is needed because of
@@ -532,23 +546,23 @@ ContentPrefService2.prototype = {
DELETE FROM settings
WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs)
`));
let prefs = new ContentPrefStore();
let isPrivate = context && context.usePrivateBrowsing;
this._execStmts(stmts, {
- onRow: function onRow(row) {
+ onRow: row => {
let grp = row.getResultByName("grp");
let name = row.getResultByName("name");
prefs.set(grp, name, undefined);
this._cache.set(grp, name, undefined);
},
- onDone: function onDone(reason, ok) {
+ onDone: (reason, ok) => {
if (ok && isPrivate) {
for (let [sgroup, sname, ] of this._pbStore) {
if (!group ||
(!includeSubdomains && group == sgroup) ||
(includeSubdomains && sgroup && this._pbStore.groupsMatchIncludingSubdomains(group, sgroup))) {
prefs.set(sgroup, sname, undefined);
this._pbStore.remove(sgroup, sname);
}
@@ -556,17 +570,17 @@ ContentPrefService2.prototype = {
}
cbHandleCompletion(callback, reason);
if (ok) {
for (let [sgroup, sname, ] of prefs) {
this._notifyPrefRemoved(sgroup, sname, isPrivate);
}
}
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
_removeAllDomainsSince: function CPS2__removeAllDomainsSince(since, context, callback) {
checkCallbackArg(callback, false);
@@ -598,23 +612,23 @@ ContentPrefService2.prototype = {
stmts.push(stmt);
// Cleanup no longer used values.
stmts = stmts.concat(this._settingsAndGroupsCleanupStmts());
let prefs = new ContentPrefStore();
let isPrivate = context && context.usePrivateBrowsing;
this._execStmts(stmts, {
- onRow: function onRow(row) {
+ onRow: row => {
let grp = row.getResultByName("grp");
let name = row.getResultByName("name");
prefs.set(grp, name, undefined);
this._cache.set(grp, name, undefined);
},
- onDone: function onDone(reason, ok) {
+ onDone: (reason, ok) => {
// This nukes all the groups in _pbStore since we don't have their timestamp
// information.
if (ok && isPrivate) {
for (let [sgroup, sname, ] of this._pbStore) {
if (sgroup) {
prefs.set(sgroup, sname, undefined);
}
}
@@ -622,17 +636,17 @@ ContentPrefService2.prototype = {
}
cbHandleCompletion(callback, reason);
if (ok) {
for (let [sgroup, sname, ] of prefs) {
this._notifyPrefRemoved(sgroup, sname, isPrivate);
}
}
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
removeAllDomainsSince: function CPS2_removeAllDomainsSince(since, context, callback) {
this._removeAllDomainsSince(since, context, callback);
},
@@ -690,65 +704,56 @@ ContentPrefService2.prototype = {
SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL
)
`));
let prefs = new ContentPrefStore();
let isPrivate = context && context.usePrivateBrowsing;
this._execStmts(stmts, {
- onRow: function onRow(row) {
+ onRow: row => {
let grp = row.getResultByName("grp");
prefs.set(grp, name, undefined);
this._cache.set(grp, name, undefined);
},
- onDone: function onDone(reason, ok) {
+ onDone: (reason, ok) => {
if (ok && isPrivate) {
for (let [sgroup, sname, ] of this._pbStore) {
if (sname === name) {
prefs.set(sgroup, name, undefined);
this._pbStore.remove(sgroup, name);
}
}
}
cbHandleCompletion(callback, reason);
if (ok) {
for (let [sgroup, , ] of prefs) {
this._notifyPrefRemoved(sgroup, name, isPrivate);
}
}
},
- onError: function onError(nsresult) {
+ onError: nsresult => {
cbHandleError(callback, nsresult);
}
});
},
- destroy: function CPS2_destroy() {
- if (this._statements) {
- for (let sql in this._statements) {
- let stmt = this._statements[sql];
- stmt.finalize();
- }
- }
- },
-
/**
* Returns the cached mozIStorageAsyncStatement for the given SQL. If no such
* statement is cached, one is created and cached.
*
* @param sql The SQL query string.
* @return The cached, possibly new, statement.
*/
- _stmt: function CPS2__stmt(sql) {
- if (!this._statements)
- this._statements = {};
- if (!this._statements[sql])
- this._statements[sql] = this._dbConnection.createAsyncStatement(sql);
- return this._statements[sql];
+ _stmt: function CPS2__stmt(sql, cachable = true) {
+ return {
+ sql,
+ cachable,
+ params: {},
+ };
},
/**
* Executes some async statements.
*
* @param stmts An array of mozIStorageAsyncStatements.
* @param callbacks An object with the following methods:
* onRow(row) (optional)
@@ -758,52 +763,53 @@ ContentPrefService2.prototype = {
* Called when done.
* reason: A nsIContentPrefService2.COMPLETE_* value.
* reasonOK: reason == nsIContentPrefService2.COMPLETE_OK.
* didGetRow: True if onRow was ever called.
* onError(nsresult) (optional)
* Called on error.
* nsresult: The error code.
*/
- _execStmts: function CPS2__execStmts(stmts, callbacks) {
- let self = this;
+ _execStmts: async function CPS2__execStmts(stmts, callbacks) {
+ let conn = await this.conn;
+ let ok = true;
let gotRow = false;
- this._dbConnection.executeAsync(stmts, stmts.length, {
- handleResult: function handleResult(results) {
+ let { onRow, onError } = callbacks;
+ await conn.executeTransaction(async () => {
+ for (let {sql, params, cachable} of stmts) {
try {
- let row = null;
- while ((row = results.getNextRow())) {
+ let execute = cachable ? conn.executeCached : conn.execute;
+ await execute.call(conn, sql, params, row => {
gotRow = true;
- if (callbacks.onRow)
- callbacks.onRow.call(self, row);
+ if (onRow) {
+ try {
+ onRow(row);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ });
+ } catch (e) {
+ try {
+ onError(Cr.NS_ERROR_FAILURE);
+ } catch (err) {
+ ok = false;
+ Cu.reportError(e);
}
- } catch (err) {
- Cu.reportError(err);
- }
- },
- handleCompletion: function handleCompletion(reason) {
- try {
- let ok = reason == Ci.mozIStorageStatementCallback.REASON_FINISHED;
- callbacks.onDone.call(self,
- ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
- Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
- ok, gotRow);
- } catch (err) {
- Cu.reportError(err);
- }
- },
- handleError: function handleError(error) {
- try {
- if (callbacks.onError)
- callbacks.onError.call(self, Cr.NS_ERROR_FAILURE);
- } catch (err) {
- Cu.reportError(err);
}
}
});
+
+ try {
+ callbacks.onDone(ok ? Ci.nsIContentPrefCallback2.COMPLETE_OK :
+ Ci.nsIContentPrefCallback2.COMPLETE_ERROR,
+ ok, gotRow);
+ } catch (e) {
+ Cu.reportError(e);
+ }
},
__grouper: null,
get _grouper() {
if (!this.__grouper)
this.__grouper = Cc["@mozilla.org/content-pref/hostname-grouper;1"].
getService(Ci.nsIContentURIGrouper);
return this.__grouper;
@@ -916,48 +922,50 @@ ContentPrefService2.prototype = {
* Tests use this as a backchannel by calling it directly.
*
* @param subj This value depends on topic.
* @param topic The backchannel "method" name.
* @param data This value depends on topic.
*/
observe: function CPS2_observe(subj, topic, data) {
switch (topic) {
- case "xpcom-shutdown":
+ case "profile-before-change":
this._destroy();
break;
case "last-pb-context-exited":
this._pbStore.removeAll();
break;
case "test:reset":
let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
this._reset(fn);
break;
case "test:db":
let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get();
- obj.value = this._dbConnection;
+ obj.value = this.conn;
break;
}
},
/**
* Removes all state from the service. Used by tests.
*
* @param callback A function that will be called when done.
*/
- _reset: function CPS2__reset(callback) {
+ async _reset(callback) {
this._pbStore.removeAll();
this._cache.removeAll();
this._observers = {};
this._genericObservers = [];
let tables = ["prefs", "groups", "settings"];
let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`));
- this._execStmts(stmts, { onDone: () => callback() });
+ this._execStmts(stmts, { onDone: () => {
+ callback();
+ } });
},
QueryInterface: function CPS2_QueryInterface(iid) {
let supportedIIDs = [
Ci.nsIContentPrefService2,
Ci.nsIObserver,
Ci.nsISupports,
];
@@ -996,196 +1004,189 @@ ContentPrefService2.prototype = {
},
prefs_idx: {
table: "prefs",
columns: ["timestamp", "groupID", "settingID"]
}
}
},
- _dbConnection: null,
+ _debugLog: false,
- // _dbInit and the methods it calls (_dbCreate, _dbMigrate, and version-
- // specific migration methods) must be careful not to call any method
- // of the service that assumes the database connection has already been
- // initialized, since it won't be initialized until at the end of _dbInit.
-
- _dbInit: function ContentPrefService__dbInit() {
- var dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
- dbFile.append("content-prefs.sqlite");
+ log: function CPS2_log(aMessage) {
+ if (this._debugLog) {
+ Services.console.logStringMessage("ContentPrefService2: " + aMessage);
+ }
+ },
- var dbConnection;
+ async _getConnection(aAttemptNum = 0) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, "content-prefs.sqlite");
+ let conn;
+ let resetAndRetry = async e => {
+ if (e.status != Cr.NS_ERROR_FILE_CORRUPTED) {
+ throw e;
+ }
- if (!dbFile.exists())
- dbConnection = this._dbCreate(dbFile);
- else {
- try {
- dbConnection = Services.storage.openDatabase(dbFile);
- } catch (e) {
- // If the connection isn't ready after we open the database, that means
- // the database has been corrupted, so we back it up and then recreate it.
- if (e.result != Cr.NS_ERROR_FILE_CORRUPTED)
- throw e;
- dbConnection = this._dbBackUpAndRecreate(dbFile, dbConnection);
+ if (aAttemptNum >= this.MAX_ATTEMPTS) {
+ if (conn) {
+ await conn.close();
+ }
+ this.log("Establishing connection failed too many times. Giving up.");
+ throw e;
}
- // Get the version of the schema in the file.
- var version = dbConnection.schemaVersion;
+ try {
+ await this._failover(conn, path);
+ } catch (e) {
+ Cu.reportError(e);
+ throw e;
+ }
+ return this._getConnection(++aAttemptNum);
+ };
+ try {
+ conn = await Sqlite.openConnection({ path });
+ Sqlite.shutdown.addBlocker(
+ "Closing ContentPrefService2 connection.",
+ () => conn.close());
+ } catch (e) {
+ Cu.reportError(e);
+ return resetAndRetry(e);
+ }
- // Try to migrate the schema in the database to the current schema used by
- // the service. If migration fails, back up the database and recreate it.
- if (version != this._dbVersion) {
- try {
- this._dbMigrate(dbConnection, version, this._dbVersion);
- } catch (ex) {
- Cu.reportError("error migrating DB: " + ex + "; backing up and recreating");
- dbConnection = this._dbBackUpAndRecreate(dbFile, dbConnection);
- }
- }
+ try {
+ await this._dbMaybeInit(conn);
+ } catch (e) {
+ Cu.reportError(e);
+ return resetAndRetry(e);
}
// Turn off disk synchronization checking to reduce disk churn and speed up
// operations when prefs are changed rapidly (such as when a user repeatedly
// changes the value of the browser zoom setting for a site).
//
// Note: this could cause database corruption if the OS crashes or machine
// loses power before the data gets written to disk, but this is considered
// a reasonable risk for the not-so-critical data stored in this database.
//
// If you really don't want to take this risk, however, just set the
// toolkit.storage.synchronous pref to 1 (NORMAL synchronization) or 2
// (FULL synchronization), in which case mozStorageConnection::Initialize
// will use that value, and we won't override it here.
if (!Services.prefs.prefHasUserValue("toolkit.storage.synchronous"))
- dbConnection.executeSimpleSQL("PRAGMA synchronous = OFF");
+ await conn.execute("PRAGMA synchronous = OFF");
- this._dbConnection = dbConnection;
+ return conn;
},
- _dbCreate: function ContentPrefService__dbCreate(aDBFile) {
- var dbConnection = Services.storage.openDatabase(aDBFile);
-
- try {
- this._dbCreateSchema(dbConnection);
- dbConnection.schemaVersion = this._dbVersion;
- } catch (ex) {
- // If we failed to create the database (perhaps because the disk ran out
- // of space), then remove the database file so we don't leave it in some
- // half-created state from which we won't know how to recover.
- dbConnection.close();
- aDBFile.remove(false);
- throw ex;
+ async _failover(aConn, aPath) {
+ this.log("Cleaning up DB file - close & remove & backup.");
+ if (aConn) {
+ await aConn.close();
}
-
- return dbConnection;
+ let backupFile = aPath + ".corrupt";
+ let { file, path: uniquePath } =
+ await OS.File.openUnique(backupFile, { humanReadable: true });
+ await file.close();
+ await OS.File.copy(aPath, uniquePath);
+ await OS.File.remove(aPath);
+ this.log("Completed DB cleanup.");
},
- _dbCreateSchema: function ContentPrefService__dbCreateSchema(aDBConnection) {
- this._dbCreateTables(aDBConnection);
- this._dbCreateIndices(aDBConnection);
- },
+ _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) {
+ let version = parseInt(await aConn.getSchemaVersion(), 10);
+ this.log("Schema version: " + version);
- _dbCreateTables: function ContentPrefService__dbCreateTables(aDBConnection) {
- for (let name in this._dbSchema.tables)
- aDBConnection.createTable(name, this._dbSchema.tables[name]);
- },
-
- _dbCreateIndices: function ContentPrefService__dbCreateIndices(aDBConnection) {
- for (let name in this._dbSchema.indices) {
- let index = this._dbSchema.indices[name];
- let statement = `
- CREATE INDEX IF NOT EXISTS ${name} ON ${index.table}
- (${index.columns.join(", ")})
- `;
- aDBConnection.executeSimpleSQL(statement);
+ if (version == 0) {
+ await this._dbCreateSchema(aConn);
+ } else if (version != this._dbVersion) {
+ await this._dbMigrate(aConn, version, this._dbVersion);
}
},
- _dbBackUpAndRecreate: function ContentPrefService__dbBackUpAndRecreate(aDBFile,
- aDBConnection) {
- Services.storage.backupDatabaseFile(aDBFile, "content-prefs.sqlite.corrupt");
+ _createTable: async function CPS2__createTable(aConn, aName) {
+ let tSQL = this._dbSchema.tables[aName];
+ this.log("Creating table " + aName + " with " + tSQL);
+ await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`);
+ },
- // Close the database, ignoring the "already closed" exception, if any.
- // It'll be open if we're here because of a migration failure but closed
- // if we're here because of database corruption.
- try { aDBConnection.close(); } catch (ex) {}
-
- aDBFile.remove(false);
-
- let dbConnection = this._dbCreate(aDBFile);
-
- return dbConnection;
+ _createIndex: async function CPS2__createTable(aConn, aName) {
+ let index = this._dbSchema.indices[aName];
+ let statement = "CREATE INDEX IF NOT EXISTS " + aName + " ON " + index.table +
+ "(" + index.columns.join(", ") + ")";
+ await aConn.execute(statement);
},
- _dbMigrate: function ContentPrefService__dbMigrate(aDBConnection, aOldVersion, aNewVersion) {
+ _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) {
+ await aConn.executeTransaction(async () => {
+ this.log("Creating DB -- tables");
+ for (let name in this._dbSchema.tables) {
+ await this._createTable(aConn, name);
+ }
+
+ this.log("Creating DB -- indices");
+ for (let name in this._dbSchema.indices) {
+ await this._createIndex(aConn, name);
+ }
+
+ await aConn.setSchemaVersion(this._dbVersion);
+ });
+ },
+
+ _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) {
/**
* Migrations should follow the template rules in bug 1074817 comment 3 which are:
* 1. Migration should be incremental and non-breaking.
* 2. It should be idempotent because one can downgrade an upgrade again.
* On downgrade:
* 1. Decrement schema version so that upgrade runs the migrations again.
*/
- aDBConnection.beginTransaction();
-
- try {
- /**
- * If the schema version is 0, that means it was never set, which means
- * the database was somehow created without the schema being applied, perhaps
- * because the system ran out of disk space (although we check for this
- * in _createDB) or because some other code created the database file without
- * applying the schema. In any case, recover by simply reapplying the schema.
- */
- if (aOldVersion == 0) {
- this._dbCreateSchema(aDBConnection);
- } else {
- for (let i = aOldVersion; i < aNewVersion; i++) {
- let migrationName = "_dbMigrate" + i + "To" + (i + 1);
- if (typeof this[migrationName] != "function") {
- throw new Error("no migrator function from version " + aOldVersion + " to version " +
- aNewVersion);
- }
- this[migrationName](aDBConnection);
+ await aConn.executeTransaction(async () => {
+ for (let i = aOldVersion; i < aNewVersion; i++) {
+ let migrationName = "_dbMigrate" + i + "To" + (i + 1);
+ if (typeof this[migrationName] != "function") {
+ throw new Error("no migrator function from version " + aOldVersion + " to version " +
+ aNewVersion);
}
+ await this[migrationName](aConn);
}
- aDBConnection.schemaVersion = aNewVersion;
- aDBConnection.commitTransaction();
- } catch (ex) {
- aDBConnection.rollbackTransaction();
- throw ex;
- }
+ await aConn.setSchemaVersion(aNewVersion);
+ });
},
- _dbMigrate1To2: function ContentPrefService___dbMigrate1To2(aDBConnection) {
- aDBConnection.executeSimpleSQL("ALTER TABLE groups RENAME TO groupsOld");
- aDBConnection.createTable("groups", this._dbSchema.tables.groups);
- aDBConnection.executeSimpleSQL(`
+ _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) {
+ await aConn.execute("ALTER TABLE groups RENAME TO groupsOld");
+ await this._createTable(aConn, "groups");
+ await aConn.execute(`
INSERT INTO groups (id, name)
SELECT id, name FROM groupsOld
`);
- aDBConnection.executeSimpleSQL("DROP TABLE groupers");
- aDBConnection.executeSimpleSQL("DROP TABLE groupsOld");
- },
-
- _dbMigrate2To3: function ContentPrefService__dbMigrate2To3(aDBConnection) {
- this._dbCreateIndices(aDBConnection);
+ await aConn.execute("DROP TABLE groupers");
+ await aConn.execute("DROP TABLE groupsOld");
},
- _dbMigrate3To4: function ContentPrefService__dbMigrate3To4(aDBConnection) {
+ _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) {
+ for (let name in this._dbSchema.indices) {
+ await this._createIndex(aConn, name);
+ }
+ },
+
+ _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) {
// Add timestamp column if it does not exist yet. This operation is idempotent.
try {
- let stmt = aDBConnection.createStatement("SELECT timestamp FROM prefs");
- stmt.finalize();
+ await aConn.execute("SELECT timestamp FROM prefs");
} catch (e) {
- aDBConnection.executeSimpleSQL("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
+ await aConn.execute("ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0");
}
// To modify prefs_idx drop it and create again.
- aDBConnection.executeSimpleSQL("DROP INDEX IF EXISTS prefs_idx");
- this._dbCreateIndices(aDBConnection);
+ await aConn.execute("DROP INDEX IF EXISTS prefs_idx");
+ for (let name in this._dbSchema.indices) {
+ await this._createIndex(aConn, name);
+ }
},
};
function checkGroupArg(group) {
if (!group || typeof(group) != "string")
throw invalidArg("domain must be nonempty string.");
}