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.
This commit is contained in:
Dan Stillman 2013-03-22 02:18:00 -04:00
parent 6896beb096
commit 97358aad7a
3 changed files with 290 additions and 59 deletions

View File

@ -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
*/

View File

@ -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>

View File

@ -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>