diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index 17bea222c7..4ebb920dfd 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -1653,19 +1653,19 @@ Zotero.Items = function() { /** * Purge unused data values */ - this.purge = Zotero.Promise.coroutine(function* () { - Zotero.DB.requireTransaction(); - + this.purge = async function () { if (!Zotero.Prefs.get('purge.items')) { return; } - var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN " - + "(SELECT valueID FROM itemData)"; - yield Zotero.DB.queryAsync(sql); + await Zotero.DB.executeTransaction(async function () { + let sql = "DELETE FROM itemDataValues WHERE valueID NOT IN " + + "(SELECT valueID FROM itemData)"; + await Zotero.DB.queryAsync(sql, [], { ignoreDBLock: true }); + }, { disableForeignKeys: true }); Zotero.Prefs.set('purge.items', false) - }); + }; diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js index 84cac5b53c..34f7bd4375 100644 --- a/chrome/content/zotero/xpcom/db.js +++ b/chrome/content/zotero/xpcom/db.js @@ -419,12 +419,14 @@ Zotero.DBConnection.prototype.getNextName = async function (libraryID, table, fi /** * @param {Function} func - Async function containing `await Zotero.DB.queryAsync()` and similar * @param {Object} [options] - * @param {Boolean} [options.disableForeignKeys] - Disable foreign key constraints before - * transaction and re-enable after. (`PRAGMA foreign_keys=0|1` is a no-op during a transaction.) + * @param {Boolean} [options.disableForeignKeys] - Disable foreign key checks before the + * transaction and re-enable after, while preventing any other queries from running. + * `queryAsync()` and similar within `func` must pass `ignoreDBLock: true` or they'll hang. + * (`PRAGMA foreign_keys=OFF|ON` is a no-op during a transaction, so it can't just be set within + * the function.) * @return {Promise} - Promise for result of generator function */ -Zotero.DBConnection.prototype.executeTransaction = async function (func, options) { - options = options || {}; +Zotero.DBConnection.prototype.executeTransaction = async function (func, options = {}) { var resolve; // Set temporary options for this transaction that will be reset at the end @@ -462,34 +464,49 @@ Zotero.DBConnection.prototype.executeTransaction = async function (func, options } } - if (options.disableForeignKeys) { - await this.queryAsync("PRAGMA foreign_keys = 0"); + let result; + let resolveDBLockPromise; + try { + let conn = this._getConnection(options) || (await this._getConnectionAsync(options)); + + if (func.constructor.name == 'GeneratorFunction') { + throw new Error("Zotero.DB.executeTransaction() no longer takes a generator function " + + "-- pass an async function instead"); + } + + if (options.disableForeignKeys) { + this._dbLockPromise = new Promise(function () { + resolveDBLockPromise = arguments[0]; + }); + await this.queryAsync("PRAGMA foreign_keys=OFF", [], { ignoreDBLock: true }); + } + + result = await conn.executeTransaction(func); + Zotero.debug(`Committed DB transaction ${id}`, 4); } - - var conn = this._getConnection(options) || (await this._getConnectionAsync(options)); - - if (func.constructor.name == 'GeneratorFunction') { - throw new Error("Zotero.DB.executeTransaction() no longer takes a generator function " - + "-- pass an async function instead"); + finally { + if (options.disableForeignKeys) { + await this.queryAsync("PRAGMA foreign_keys=ON", [], { ignoreDBLock: true }); + if (resolveDBLockPromise) { + resolveDBLockPromise(); + this._dbLockPromise = undefined; + } + } } - var result = await conn.executeTransaction(func); - Zotero.debug(`Committed DB transaction ${id}`, 4); - // Clear transaction time if (this._transactionDate) { this._transactionDate = null; } + this._transactionID = null; + if (options.vacuumOnCommit) { Zotero.debug('Vacuuming database'); await this.queryAsync('VACUUM'); Zotero.debug('Done vacuuming'); - } - this._transactionID = null; - // Function to run once transaction has been committed but before any // permanent callbacks if (options.onCommit) { @@ -546,10 +563,6 @@ Zotero.DBConnection.prototype.executeTransaction = async function (func, options throw e; } finally { - if (options.disableForeignKeys) { - await this.queryAsync("PRAGMA foreign_keys = 1"); - } - // Reset options back to their previous values if (options) { for (let option in options) { @@ -597,13 +610,19 @@ Zotero.DBConnection.prototype.requireTransaction = function () { * rows are Proxy objects that return values from the * underlying mozIStorageRows based on column names. */ -Zotero.DBConnection.prototype.queryAsync = async function (sql, params, options) { +Zotero.DBConnection.prototype.queryAsync = async function (sql, params, options = {}) { try { let onRow = null; let conn = this._getConnection(options) || (await this._getConnectionAsync(options)); if (!options || !options.noParseParams) { [sql, params] = this.parseQueryAndParams(sql, params); } + + if (this._dbLockPromise && !options.ignoreDBLock) { + Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2); + await this._dbLockPromise; + } + if (Zotero.Debug.enabled) { this.logQuery(sql, params, options); } @@ -719,6 +738,12 @@ Zotero.DBConnection.prototype.valueQueryAsync = async function (sql, params, opt try { let conn = this._getConnection(options) || (await this._getConnectionAsync(options)); [sql, params] = this.parseQueryAndParams(sql, params); + + if (this._dbLockPromise && !options.ignoreDBLock) { + Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2); + await this._dbLockPromise; + } + if (Zotero.Debug.enabled) { this.logQuery(sql, params, options); } @@ -766,6 +791,12 @@ Zotero.DBConnection.prototype.columnQueryAsync = async function (sql, params, op try { let conn = this._getConnection(options) || (await this._getConnectionAsync(options)); [sql, params] = this.parseQueryAndParams(sql, params); + + if (this._dbLockPromise && !options.ignoreDBLock) { + Zotero.debug(`Waiting for DB lock to be released: ${sql}`, 2); + await this._dbLockPromise; + } + if (Zotero.Debug.enabled) { this.logQuery(sql, params, options); } diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index 56b44874cc..99d72870a6 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -600,9 +600,15 @@ Zotero.Schema = new function(){ this._updateGlobalSchemaForTest = async function (schema) { - await Zotero.DB.executeTransaction(async function () { - await _updateGlobalSchema(schema); - }.bind(this), { disableForeignKeys: true }); + await Zotero.DB.queryAsync("PRAGMA foreign_keys=OFF"); + try { + await Zotero.DB.executeTransaction(async function () { + await _updateGlobalSchema(schema); + }); + } + finally { + await Zotero.DB.queryAsync("PRAGMA foreign_keys=ON"); + } }; @@ -733,58 +739,64 @@ Zotero.Schema = new function(){ var itemTypeID = Zotero.ID.get('customItemTypes'); - yield Zotero.DB.executeTransaction(async function () { - await Zotero.DB.queryAsync("INSERT INTO customItemTypes VALUES (?, 'nsfReviewer', 'NSF Reviewer', 1, 'chrome://zotero/skin/report_user.png')", itemTypeID); - - var fields = [ - ['name', 'Name'], - ['institution', 'Institution'], - ['address', 'Address'], - ['telephone', 'Telephone'], - ['email', 'Email'], - ['homepage', 'Webpage'], - ['discipline', 'Discipline'], - ['nsfID', 'NSF ID'], - ['dateSent', 'Date Sent'], - ['dateDue', 'Date Due'], - ['accepted', 'Accepted'], - ['programDirector', 'Program Director'] - ]; - for (var i=0; i