www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 97358aad7ac16878b0a14dcb65938ea8df310f55
parent 6896beb0965d5cb20b60b37b96494c95b46f9147
Author: Dan Stillman <dstillman@zotero.org>
Date:   Fri, 22 Mar 2013 02:18:00 -0400

Asynchronous DB query methods (experimental)

See comment in db.js for example usage.

This requires Firefox 20 or later unless we bundle the necessary code
modules ourselves.

Diffstat:
Mchrome/content/zotero/xpcom/db.js | 341++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Minstall.rdf | 4++--
Mupdate.rdf | 4++--
3 files changed, 290 insertions(+), 59 deletions(-)

diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js @@ -34,6 +34,18 @@ Zotero.DBConnection = function(dbName) { throw ('DB name not provided in Zotero.DBConnection()'); } + // Code modules for async methods + // Fx21+ + try { + Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", this); + } + // Fx20 + catch (e) { + Components.utils.import("resource://gre/modules/commonjs/promise/core.js", this); + } + Components.utils.import("resource://gre/modules/Task.jsm", this); + Components.utils.import("resource://gre/modules/Sqlite.jsm", this); + this.skipBackup = false; this.transactionVacuum = false; @@ -67,6 +79,7 @@ Zotero.DBConnection = function(dbName) { this._dbName = dbName; this._shutdown = false; this._connection = null; + this._connectionAsync = null; this._transactionDate = null; this._lastTransactionDate = null; this._transactionRollback = null; @@ -238,61 +251,7 @@ Zotero.DBConnection.prototype.getStatement = function (sql, params, checkParams) var matches = sql.match(/^[^\s\(]*/); var queryMethod = matches[0].toLowerCase(); - if (params) { - // If single scalar value or single non-array object, wrap in an array - if (typeof params != 'object' || params === null || - (params && typeof params == 'object' && !params.length)) { - var params = [params]; - } - - // Since we might make changes, only work on a copy of the array - var params = params.concat(); - - // Replace NULL bound parameters with hard-coded NULLs - var nullRE = /\s*=?\s*\?/g; - // Reset lastIndex, since regexp isn't recompiled dynamically - nullRE.lastIndex = 0; - var lastNullParamIndex = -1; - for (var i=0; i<params.length; i++) { - if (typeof params[i] != 'object' || params[i] !== null) { - continue; - } - - // Find index of this parameter, skipping previous ones - do { - var matches = nullRE.exec(sql); - lastNullParamIndex++; - } - while (lastNullParamIndex < i); - lastNullParamIndex = i; - - if (matches[0].indexOf('=') == -1) { - // mozStorage supports null bound parameters in value lists (e.g., "(?,?)") natively - continue; - //var repl = 'NULL'; - } - else if (queryMethod == 'select') { - var repl = ' IS NULL'; - } - else { - var repl = '=NULL'; - } - - var subpos = matches.index; - var sublen = matches[0].length; - sql = sql.substring(0, subpos) + repl + sql.substr(subpos + sublen); - - //Zotero.debug("Hard-coding null bound parameter " + i); - - params.splice(i, 1); - i--; - lastNullParamIndex--; - continue; - } - if (!params.length) { - params = undefined; - } - } + [sql, params] = this.parseQueryAndParams(sql, params); try { this._debug(sql,5); @@ -411,6 +370,67 @@ Zotero.DBConnection.prototype.getStatement = function (sql, params, checkParams) } +Zotero.DBConnection.prototype.parseQueryAndParams = function (sql, params) { + if (params) { + // If single scalar value or single non-array object, wrap in an array + if (typeof params != 'object' || params === null || + (typeof params == 'object' && !params.length)) { + params = [params]; + } + // Since we might make changes, only work on a copy of the array + else { + params = params.concat(); + } + + // Replace NULL bound parameters with hard-coded NULLs + var nullRE = /\s*=?\s*\?/g; + // Reset lastIndex, since regexp isn't recompiled dynamically + nullRE.lastIndex = 0; + var lastNullParamIndex = -1; + for (var i=0; i<params.length; i++) { + if (params[i] !== null) { + continue; + } + + // Find index of this parameter, skipping previous ones + do { + var matches = nullRE.exec(sql); + lastNullParamIndex++; + } + while (lastNullParamIndex < i); + lastNullParamIndex = i; + + if (matches[0].indexOf('=') == -1) { + // mozStorage supports null bound parameters in value lists (e.g., "(?,?)") natively + continue; + } + else if (queryMethod == 'select') { + var repl = ' IS NULL'; + } + else { + var repl = '=NULL'; + } + + var subpos = matches.index; + var sublen = matches[0].length; + sql = sql.substring(0, subpos) + repl + sql.substr(subpos + sublen); + + //Zotero.debug("Hard-coding null bound parameter " + i); + + params.splice(i, 1); + i--; + lastNullParamIndex--; + continue; + } + if (!params.length) { + params = undefined; + } + } + + return [sql, params]; +}; + + /* * Only for use externally with this.getStatement() */ @@ -738,6 +758,217 @@ Zotero.DBConnection.prototype.getNextName = function (table, field, name) } +// +// Async methods +// +// +// Zotero.DB.executeTransaction(function (conn) { +// var created = yield Zotero.DB.queryAsync("CREATE TEMPORARY TABLE tmpFoo (foo TEXT, bar INT)"); +// +// // created == true +// +// var result = yield Zotero.DB.queryAsync("INSERT INTO tmpFoo VALUES ('a', ?)", 1); +// +// // result == 1 +// +// yield Zotero.DB.queryAsync("INSERT INTO tmpFoo VALUES ('b', 2)"); +// yield Zotero.DB.queryAsync("INSERT INTO tmpFoo VALUES ('c', 3)"); +// yield Zotero.DB.queryAsync("INSERT INTO tmpFoo VALUES ('d', 4)"); +// +// var value = yield Zotero.DB.valueQueryAsync("SELECT foo FROM tmpFoo WHERE bar=?", 2); +// +// // value == "b" +// +// var vals = yield Zotero.DB.columnQueryAsync("SELECT foo FROM tmpFoo"); +// +// // '0' => "a" +// // '1' => "b" +// // '2' => "c" +// // '3' => "d" +// +// let rows = yield Zotero.DB.queryAsync("SELECT * FROM tmpFoo"); +// for each(let row in rows) { +// // row.foo == 'a', row.bar == 1 +// // row.foo == 'b', row.bar == 2 +// // row.foo == 'c', row.bar == 3 +// // row.foo == 'd', row.bar == 4 +// } +// +// // Optional, but necessary to pass 'rows' on to the next handler +// Zotero.DB.asyncResult(rows); +// ) +// then(function (rows) { +// // rows == same as above +// ) +// .done(); +// +/** + * @param {Function} func Task.js-style generator function that yields promises, + * generally from queryAsync() and similar + * @return {Promise} Q promise for result of generator function, which can + * pass a result by calling asyncResult(val) at the end + */ +Zotero.DBConnection.prototype.executeTransaction = function (func) { + return Q( + this._getConnectionAsync() + .then(function (conn) { + return conn.executeTransaction(func); + }) + ); +}; + + +/** + * @param {String} sql SQL statement to run + * @param {Array|String|Integer} [params] SQL parameters to bind + * @return {Promise|FALSE} A Q promise for an array of rows, or FALSE if none. + * The individual rows are Proxy objects that return + * values from the underlying mozIStorageRows based + * on column names. + */ +Zotero.DBConnection.prototype.queryAsync = function (sql, params) { + let conn; + let self = this; + return this._getConnectionAsync(). + then(function (c) { + conn = c; + [sql, params] = self.parseQueryAndParams(sql, params); + Zotero.debug(sql, 5); + return conn.executeCached(sql, params); + }) + .then(function (rows) { + // Parse out the SQL command being used + var op = sql.match(/^[^a-z]*[^ ]+/i); + if (op) { + op = op.toString().toLowerCase(); + } + + // If SELECT statement, return result + if (op == 'select') { + // Fake an associative array with a proxy + let handler = { + get: function(target, name) { + return target.getResultByName(name); + } + }; + for (let i=0, len=rows.length; i<len; i++) { + rows[i] = new Proxy(rows[i], handler); + } + return rows; + } + else { + if (op == 'insert' || op == 'replace') { + return conn.lastInsertRowID; + } + else if (op == 'create') { + return true; + } + else { + return conn.affectedRows; + } + } + }); +}; + + +/** + * @param {String} sql SQL statement to run + * @param {Array|String|Integer} [params] SQL parameters to bind + * @return {Promise|FALSE} A Q promise for the value, or FALSE if no rows + */ +Zotero.DBConnection.prototype.valueQueryAsync = function (sql, params) { + let self = this; + return this._getConnectionAsync(). + then(function (conn) { + [sql, params] = self.parseQueryAndParams(sql, params); + Zotero.debug(sql, 5); + return conn.executeCached(sql, params); + }) + .then(function (rows) { + return rows.length ? self._getTypedValue(rows[0], 0) : false; + }); +}; + + +/** + * DEBUG: This doesn't work -- returning the mozIStorageValueArray Proxy + * seems to break things + * + * @param {String} sql SQL statement to run + * @param {Array|String|Integer} [params] SQL parameters to bind + * @return {Promise|FALSE} A Q promise for the row, or FALSE if no rows + */ +Zotero.DBConnection.prototype.rowQueryAsync = function (sql, params) { + let self = this; + return this.queryAsync(sql, params) + .then(function (rows) { + return rows.length ? rows[0] : false; + }); +}; + + +/** + * @param {String} sql SQL statement to run + * @param {Array|String|Integer} [params] SQL parameters to bind + * @return {Promise|FALSE} A Q promise for the column, or FALSE if no rows + */ +Zotero.DBConnection.prototype.columnQueryAsync = function (sql, params) { + let conn; + let self = this; + return this._getConnectionAsync(). + then(function (c) { + conn = c; + [sql, params] = self.parseQueryAndParams(sql, params); + Zotero.debug(sql, 5); + return conn.executeCached(sql, params); + }) + .then(function (rows) { + if (!rows.length) { + return false; + } + var column = []; + for (let i=0, len=rows.length; i<len; i++) { + column.push(self._getTypedValue(rows[i], 0)); + } + return column; + }); +}; + + +/** + * Generator functions can't return values, but Task.js-style generators, + * as used by executeTransaction(), can throw a special exception in order + * to do so. This function throws such an exception for passed value and + * can be used at the end of executeTransaction() to return a value to the + * next promise handler. + */ +Zotero.DBConnection.prototype.asyncResult = function (val) { + throw new this.Task.Result(val); +}; + + +/** + * Asynchronously return a connection object for the current DB + */ +Zotero.DBConnection.prototype._getConnectionAsync = function () { + if (this._connectionAsync) { + return this.Promise.resolve(this._connectionAsync); + } + + var db = this._getDBConnection(); + var options = { + path: db.databaseFile.path + }; + var self = this; + Zotero.debug("Asynchronously opening DB connection"); + return this.Sqlite.openConnection(options) + .then(function(conn) { + self._connectionAsync = conn; + return conn; + }); +}; + + /* * Implements nsIObserver */ diff --git a/install.rdf b/install.rdf @@ -24,8 +24,8 @@ <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> - <em:minVersion>17.0</em:minVersion> - <em:maxVersion>21.*</em:maxVersion> + <em:minVersion>20.0</em:minVersion> + <em:maxVersion>22.*</em:maxVersion> </Description> </em:targetApplication> diff --git a/update.rdf b/update.rdf @@ -11,8 +11,8 @@ <targetApplication> <RDF:Description> <id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</id> - <minVersion>17.0</minVersion> - <maxVersion>21.*</maxVersion> + <minVersion>20.0</minVersion> + <maxVersion>22.*</maxVersion> <updateLink>http://download.zotero.org/extension/zotero.xpi</updateLink> <updateHash>sha1:</updateHash> </RDF:Description>