author | Doug Thayer <dothayer@mozilla.com> |
Mon, 12 Mar 2018 14:41:42 -0700 | |
changeset 415934 | 29598c587f7fb07c434df30534bdf489f987562d |
parent 415933 | fec4af27b939fe5cf2a5d6ab6290e1feaa302a18 |
child 415935 | 744a7b9fe0abd1095d0f5ba464161d51a97a33b7 |
push id | 33911 |
push user | csabou@mozilla.com |
push date | Fri, 27 Apr 2018 10:01:39 +0000 |
treeherder | mozilla-central@822936017145 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | mak |
bugs | 887889 |
milestone | 61.0a1 |
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
|
toolkit/components/contentprefs/ContentPrefService2.js | file | annotate | diff | comparison | revisions |
--- 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."); }