mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 19:04:45 +00:00
Bug 813833 - Add a Promise-based API to mozIStorage/SQLite; r=mak, sr=Mossop
This commit is contained in:
parent
7996a3ff45
commit
993797122a
@ -56,6 +56,7 @@ skip-if = os == "android"
|
||||
[include:toolkit/forgetaboutsite/test/unit/xpcshell.ini]
|
||||
[include:toolkit/content/tests/unit/xpcshell.ini]
|
||||
[include:toolkit/identity/tests/unit/xpcshell.ini]
|
||||
[include:toolkit/modules/tests/xpcshell/xpcshell.ini]
|
||||
[include:toolkit/mozapps/downloads/tests/unit/xpcshell.ini]
|
||||
[include:toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini]
|
||||
[include:toolkit/mozapps/extensions/test/xpcshell-unpack/xpcshell.ini]
|
||||
|
@ -17,6 +17,7 @@ PARALLEL_DIRS = \
|
||||
forgetaboutsite \
|
||||
identity \
|
||||
locales \
|
||||
modules \
|
||||
mozapps/downloads \
|
||||
mozapps/extensions \
|
||||
mozapps/handling \
|
||||
|
18
toolkit/modules/Makefile.in
Normal file
18
toolkit/modules/Makefile.in
Normal file
@ -0,0 +1,18 @@
|
||||
# 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/.
|
||||
|
||||
DEPTH = @DEPTH@
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
TEST_DIRS += tests
|
||||
|
||||
EXTRA_JS_MODULES := \
|
||||
Sqlite.jsm \
|
||||
$(NULL)
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
648
toolkit/modules/Sqlite.jsm
Normal file
648
toolkit/modules/Sqlite.jsm
Normal file
@ -0,0 +1,648 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"Sqlite",
|
||||
];
|
||||
|
||||
const {interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
|
||||
"resource://services-common/utils.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
|
||||
"resource://gre/modules/FileUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
|
||||
// Counts the number of created connections per database basename(). This is
|
||||
// used for logging to distinguish connection instances.
|
||||
let connectionCounters = {};
|
||||
|
||||
|
||||
/**
|
||||
* Opens a connection to a SQLite database.
|
||||
*
|
||||
* The following parameters can control the connection:
|
||||
*
|
||||
* path -- (string) The filesystem path of the database file to open. If the
|
||||
* file does not exist, a new database will be created.
|
||||
*
|
||||
* sharedMemoryCache -- (bool) Whether multiple connections to the database
|
||||
* share the same memory cache. Sharing the memory cache likely results
|
||||
* in less memory utilization. However, sharing also requires connections
|
||||
* to obtain a lock, possibly making database access slower. Defaults to
|
||||
* true.
|
||||
*
|
||||
*
|
||||
* FUTURE options to control:
|
||||
*
|
||||
* special named databases
|
||||
* pragma TEMP STORE = MEMORY
|
||||
* TRUNCATE JOURNAL
|
||||
* SYNCHRONOUS = full
|
||||
*
|
||||
* @param options
|
||||
* (Object) Parameters to control connection and open options.
|
||||
*
|
||||
* @return Promise<OpenedConnection>
|
||||
*/
|
||||
function openConnection(options) {
|
||||
let log = Log4Moz.repository.getLogger("Sqlite.ConnectionOpener");
|
||||
|
||||
if (!options.path) {
|
||||
throw new Error("path not specified in connection options.");
|
||||
}
|
||||
|
||||
// Retains absolute paths and normalizes relative as relative to profile.
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, options.path);
|
||||
|
||||
let sharedMemoryCache = "sharedMemoryCache" in options ?
|
||||
options.sharedMemoryCache : true;
|
||||
|
||||
let file = FileUtils.File(path);
|
||||
let openDatabaseFn = sharedMemoryCache ?
|
||||
Services.storage.openDatabase :
|
||||
Services.storage.openUnsharedDatabase;
|
||||
|
||||
let basename = OS.Path.basename(path);
|
||||
|
||||
if (!connectionCounters[basename]) {
|
||||
connectionCounters[basename] = 1;
|
||||
}
|
||||
|
||||
let number = connectionCounters[basename]++;
|
||||
let identifier = basename + "#" + number;
|
||||
|
||||
log.info("Opening database: " + path + " (" + identifier + ")");
|
||||
try {
|
||||
let connection = openDatabaseFn(file);
|
||||
|
||||
if (!connection.connectionReady) {
|
||||
log.warn("Connection is not ready.");
|
||||
return Promise.reject(new Error("Connection is not ready."));
|
||||
}
|
||||
|
||||
return Promise.resolve(new OpenedConnection(connection, basename, number));
|
||||
} catch (ex) {
|
||||
log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
|
||||
return Promise.reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle on an opened SQLite database.
|
||||
*
|
||||
* This is essentially a glorified wrapper around mozIStorageConnection.
|
||||
* However, it offers some compelling advantages.
|
||||
*
|
||||
* The main functions on this type are `execute` and `executeCached`. These are
|
||||
* ultimately how all SQL statements are executed. It's worth explaining their
|
||||
* differences.
|
||||
*
|
||||
* `execute` is used to execute one-shot SQL statements. These are SQL
|
||||
* statements that are executed one time and then thrown away. They are useful
|
||||
* for dynamically generated SQL statements and clients who don't care about
|
||||
* performance (either their own or wasting resources in the overall
|
||||
* application). Because of the performance considerations, it is recommended
|
||||
* to avoid `execute` unless the statement you are executing will only be
|
||||
* executed once or seldomly.
|
||||
*
|
||||
* `executeCached` is used to execute a statement that will presumably be
|
||||
* executed multiple times. The statement is parsed once and stuffed away
|
||||
* inside the connection instance. Subsequent calls to `executeCached` will not
|
||||
* incur the overhead of creating a new statement object. This should be used
|
||||
* in preference to `execute` when a specific SQL statement will be executed
|
||||
* multiple times.
|
||||
*
|
||||
* Instances of this type are not meant to be created outside of this file.
|
||||
* Instead, first open an instance of `UnopenedSqliteConnection` and obtain
|
||||
* an instance of this type by calling `open`.
|
||||
*
|
||||
* FUTURE IMPROVEMENTS
|
||||
*
|
||||
* Ability to enqueue operations. Currently there can be race conditions,
|
||||
* especially as far as transactions are concerned. It would be nice to have
|
||||
* an enqueueOperation(func) API that serially executes passed functions.
|
||||
*
|
||||
* Support for SAVEPOINT (named/nested transactions) might be useful.
|
||||
*
|
||||
* @param connection
|
||||
* (mozIStorageConnection) Underlying SQLite connection.
|
||||
* @param basename
|
||||
* (string) The basename of this database name. Used for logging.
|
||||
* @param number
|
||||
* (Number) The connection number to this database.
|
||||
*/
|
||||
function OpenedConnection(connection, basename, number) {
|
||||
let log = Log4Moz.repository.getLogger("Sqlite.Connection." + basename);
|
||||
|
||||
// Automatically prefix all log messages with the identifier.
|
||||
for (let level in Log4Moz.Level) {
|
||||
if (level == "Desc") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lc = level.toLowerCase();
|
||||
log[lc] = function (msg) {
|
||||
return Log4Moz.Logger.prototype[lc].call(log, "Conn #" + number + ": " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
this._log = log;
|
||||
|
||||
this._log.info("Opened");
|
||||
|
||||
this._connection = connection;
|
||||
this._open = true;
|
||||
|
||||
this._cachedStatements = new Map();
|
||||
this._anonymousStatements = new Map();
|
||||
this._anonymousCounter = 0;
|
||||
this._inProgressStatements = new Map();
|
||||
this._inProgressCounter = 0;
|
||||
|
||||
this._inProgressTransaction = null;
|
||||
}
|
||||
|
||||
OpenedConnection.prototype = Object.freeze({
|
||||
TRANSACTION_DEFERRED: Ci.mozIStorageConnection.TRANSACTION_DEFERRED,
|
||||
TRANSACTION_IMMEDIATE: Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE,
|
||||
TRANSACTION_EXCLUSIVE: Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE,
|
||||
|
||||
get connectionReady() {
|
||||
return this._open && this._connection.connectionReady;
|
||||
},
|
||||
|
||||
/**
|
||||
* The row ID from the last INSERT operation.
|
||||
*
|
||||
* Because all statements are executed asynchronously, this could
|
||||
* return unexpected results if multiple statements are performed in
|
||||
* parallel. It is the caller's responsibility to schedule
|
||||
* appropriately.
|
||||
*
|
||||
* It is recommended to only use this within transactions (which are
|
||||
* handled as sequential statements via Tasks).
|
||||
*/
|
||||
get lastInsertRowID() {
|
||||
this._ensureOpen();
|
||||
return this._connection.lastInsertRowID;
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of rows that were changed, inserted, or deleted by the
|
||||
* last operation.
|
||||
*
|
||||
* The same caveats regarding asynchronous execution for
|
||||
* `lastInsertRowID` also apply here.
|
||||
*/
|
||||
get affectedRows() {
|
||||
this._ensureOpen();
|
||||
return this._connection.affectedRows;
|
||||
},
|
||||
|
||||
/**
|
||||
* The integer schema version of the database.
|
||||
*
|
||||
* This is 0 if not schema version has been set.
|
||||
*/
|
||||
get schemaVersion() {
|
||||
this._ensureOpen();
|
||||
return this._connection.schemaVersion;
|
||||
},
|
||||
|
||||
set schemaVersion(value) {
|
||||
this._ensureOpen();
|
||||
this._connection.schemaVersion = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*
|
||||
* This must be performed when you are finished with the database.
|
||||
*
|
||||
* Closing the database connection has the side effect of forcefully
|
||||
* cancelling all active statements. Therefore, callers should ensure that
|
||||
* all active statements have completed before closing the connection, if
|
||||
* possible.
|
||||
*
|
||||
* The returned promise will be resolved once the connection is closed.
|
||||
*
|
||||
* IMPROVEMENT: Resolve the promise to a closed connection which can be
|
||||
* reopened.
|
||||
*
|
||||
* @return Promise<>
|
||||
*/
|
||||
close: function () {
|
||||
if (!this._connection) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Abort in-progress transaction.
|
||||
if (this._inProgressTransaction) {
|
||||
this._log.warn("Transaction in progress at time of close.");
|
||||
try {
|
||||
this._connection.rollbackTransaction();
|
||||
} catch (ex) {
|
||||
this._log.warn("Error rolling back transaction: " +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
}
|
||||
this._inProgressTransaction.reject(new Error("Connection being closed."));
|
||||
this._inProgressTransaction = null;
|
||||
}
|
||||
|
||||
// Cancel any in-progress statements.
|
||||
for (let [k, statement] of this._inProgressStatements) {
|
||||
statement.cancel();
|
||||
}
|
||||
this._inProgressStatements.clear();
|
||||
|
||||
// Next we finalize all active statements.
|
||||
for (let [k, statement] of this._anonymousStatements) {
|
||||
statement.finalize();
|
||||
}
|
||||
this._anonymousStatements.clear();
|
||||
|
||||
for (let [k, statement] of this._cachedStatements) {
|
||||
statement.finalize();
|
||||
}
|
||||
this._cachedStatements.clear();
|
||||
|
||||
// This guards against operations performed between the call to this
|
||||
// function and asyncClose() finishing. See also bug 726990.
|
||||
this._open = false;
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
this._connection.asyncClose({
|
||||
complete: function () {
|
||||
this._log.info("Closed");
|
||||
this._connection = null;
|
||||
deferred.resolve();
|
||||
}.bind(this),
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute a SQL statement and cache the underlying statement object.
|
||||
*
|
||||
* This function executes a SQL statement and also caches the underlying
|
||||
* derived statement object so subsequent executions are faster and use
|
||||
* less resources.
|
||||
*
|
||||
* This function optionally binds parameters to the statement as well as
|
||||
* optionally invokes a callback for every row retrieved.
|
||||
*
|
||||
* By default, no parameters are bound and no callback will be invoked for
|
||||
* every row.
|
||||
*
|
||||
* Bound parameters can be defined as an Array of positional arguments or
|
||||
* an object mapping named parameters to their values. If there are no bound
|
||||
* parameters, the caller can pass nothing or null for this argument.
|
||||
*
|
||||
* Callers are encouraged to pass objects rather than Arrays for bound
|
||||
* parameters because they prevent foot guns. With positional arguments, it
|
||||
* is simple to modify the parameter count or positions without fixing all
|
||||
* users of the statement. Objects/named parameters are a little safer
|
||||
* because changes in order alone won't result in bad things happening.
|
||||
*
|
||||
* When `onRow` is not specified, all returned rows are buffered before the
|
||||
* returned promise is resolved. For INSERT or UPDATE statements, this has
|
||||
* no effect because no rows are returned from these. However, it has
|
||||
* implications for SELECT statements.
|
||||
*
|
||||
* If your SELECT statement could return many rows or rows with large amounts
|
||||
* of data, for performance reasons it is recommended to pass an `onRow`
|
||||
* handler. Otherwise, the buffering may consume unacceptable amounts of
|
||||
* resources.
|
||||
*
|
||||
* If a `StopIteration` is thrown during execution of an `onRow` handler,
|
||||
* the execution of the statement is immediately cancelled. Subsequent
|
||||
* rows will not be processed and no more `onRow` invocations will be made.
|
||||
* The promise is resolved immediately.
|
||||
*
|
||||
* If a non-`StopIteration` exception is thrown by the `onRow` handler, the
|
||||
* exception is logged and processing of subsequent rows occurs as if nothing
|
||||
* happened. The promise is still resolved (not rejected).
|
||||
*
|
||||
* The return value is a promise that will be resolved when the statement
|
||||
* has completed fully.
|
||||
*
|
||||
* The promise will be rejected with an `Error` instance if the statement
|
||||
* did not finish execution fully. The `Error` may have an `errors` property.
|
||||
* If defined, it will be an Array of objects describing individual errors.
|
||||
* Each object has the properties `result` and `message`. `result` is a
|
||||
* numeric error code and `message` is a string description of the problem.
|
||||
*
|
||||
* @param name
|
||||
* (string) The name of the registered statement to execute.
|
||||
* @param params optional
|
||||
* (Array or object) Parameters to bind.
|
||||
* @param onRow optional
|
||||
* (function) Callback to receive each row from result.
|
||||
*/
|
||||
executeCached: function (sql, params=null, onRow=null) {
|
||||
this._ensureOpen();
|
||||
|
||||
if (!sql) {
|
||||
throw new Error("sql argument is empty.");
|
||||
}
|
||||
|
||||
let statement = this._cachedStatements.get(sql);
|
||||
if (!statement) {
|
||||
statement = this._connection.createAsyncStatement(sql);
|
||||
this._cachedStatements.set(sql, statement);
|
||||
}
|
||||
|
||||
return this._executeStatement(sql, statement, params, onRow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute a one-shot SQL statement.
|
||||
*
|
||||
* If you find yourself feeding the same SQL string in this function, you
|
||||
* should *not* use this function and instead use `executeCached`.
|
||||
*
|
||||
* See `executeCached` for the meaning of the arguments and extended usage info.
|
||||
*
|
||||
* @param sql
|
||||
* (string) SQL to execute.
|
||||
* @param params optional
|
||||
* (Array or Object) Parameters to bind to the statement.
|
||||
* @param onRow optional
|
||||
* (function) Callback to receive result of a single row.
|
||||
*/
|
||||
execute: function (sql, params=null, onRow=null) {
|
||||
if (typeof(sql) != "string") {
|
||||
throw new Error("Must define SQL to execute as a string: " + sql);
|
||||
}
|
||||
|
||||
this._ensureOpen();
|
||||
|
||||
let statement = this._connection.createAsyncStatement(sql);
|
||||
let index = this._anonymousCounter++;
|
||||
|
||||
this._anonymousStatements.set(index, statement);
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
this._executeStatement(sql, statement, params, onRow).then(
|
||||
function onResult(rows) {
|
||||
this._anonymousStatements.delete(index);
|
||||
statement.finalize();
|
||||
deferred.resolve(rows);
|
||||
}.bind(this),
|
||||
|
||||
function onError(error) {
|
||||
this._anonymousStatements.delete(index);
|
||||
statement.finalize();
|
||||
deferred.reject(error);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a transaction is currently in progress.
|
||||
*/
|
||||
get transactionInProgress() {
|
||||
return this._open && this._connection.transactionInProgress;
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform a transaction.
|
||||
*
|
||||
* A transaction is specified by a user-supplied function that is a
|
||||
* generator function which can be used by Task.jsm's Task.spawn(). The
|
||||
* function receives this connection instance as its argument.
|
||||
*
|
||||
* The supplied function is expected to yield promises. These are often
|
||||
* promises created by calling `execute` and `executeCached`. If the
|
||||
* generator is exhausted without any errors being thrown, the
|
||||
* transaction is committed. If an error occurs, the transaction is
|
||||
* rolled back.
|
||||
*
|
||||
* The returned value from this function is a promise that will be resolved
|
||||
* once the transaction has been committed or rolled back. The promise will
|
||||
* be resolved to whatever value the supplied function resolves to. If
|
||||
* the transaction is rolled back, the promise is rejected.
|
||||
*
|
||||
* @param func
|
||||
* (function) What to perform as part of the transaction.
|
||||
* @param type optional
|
||||
* One of the TRANSACTION_* constants attached to this type.
|
||||
*/
|
||||
executeTransaction: function (func, type=this.TRANSACTION_DEFERRED) {
|
||||
this._ensureOpen();
|
||||
|
||||
if (this.transactionInProgress) {
|
||||
throw new Error("A transaction is already active. Only one transaction " +
|
||||
"can be active at a time.");
|
||||
}
|
||||
|
||||
this._log.debug("Beginning transaction");
|
||||
this._connection.beginTransactionAs(type);
|
||||
|
||||
let deferred = Promise.defer();
|
||||
this._inProgressTransaction = deferred;
|
||||
|
||||
Task.spawn(func(this)).then(
|
||||
function onSuccess (result) {
|
||||
this._connection.commitTransaction();
|
||||
this._inProgressTransaction = null;
|
||||
this._log.debug("Transaction committed.");
|
||||
|
||||
deferred.resolve(result);
|
||||
}.bind(this),
|
||||
|
||||
function onError (error) {
|
||||
this._log.warn("Error during transaction. Rolling back: " +
|
||||
CommonUtils.exceptionStr(error));
|
||||
this._connection.rollbackTransaction();
|
||||
this._inProgressTransaction = null;
|
||||
|
||||
deferred.reject(error);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a table exists in the database.
|
||||
*
|
||||
* IMPROVEMENT: Look for temporary tables.
|
||||
*
|
||||
* @param name
|
||||
* (string) Name of the table.
|
||||
*
|
||||
* @return Promise<bool>
|
||||
*/
|
||||
tableExists: function (name) {
|
||||
return this.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||||
[name])
|
||||
.then(function onResult(rows) {
|
||||
return Promise.resolve(rows.length > 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a named index exists.
|
||||
*
|
||||
* IMPROVEMENT: Look for indexes in temporary tables.
|
||||
*
|
||||
* @param name
|
||||
* (string) Name of the index.
|
||||
*
|
||||
* @return Promise<bool>
|
||||
*/
|
||||
indexExists: function (name) {
|
||||
return this.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
||||
[name])
|
||||
.then(function onResult(rows) {
|
||||
return Promise.resolve(rows.length > 0);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_executeStatement: function (sql, statement, params, onRow) {
|
||||
if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) {
|
||||
throw new Error("Statement is not ready for execution.");
|
||||
}
|
||||
|
||||
if (onRow && typeof(onRow) != "function") {
|
||||
throw new Error("onRow must be a function. Got: " + onRow);
|
||||
}
|
||||
|
||||
if (Array.isArray(params)) {
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
statement.bindByIndex(i, params[i]);
|
||||
}
|
||||
} else if (params && typeof(params) == "object") {
|
||||
for (let k in params) {
|
||||
statement.bindByName(k, params[k]);
|
||||
}
|
||||
} else if (params) {
|
||||
throw new Error("Invalid type for bound parameters. Expected Array or " +
|
||||
"object. Got: " + params);
|
||||
}
|
||||
|
||||
let index = this._inProgressCounter++;
|
||||
|
||||
let deferred = Promise.defer();
|
||||
let userCancelled = false;
|
||||
let errors = [];
|
||||
let rows = [];
|
||||
|
||||
// Don't incur overhead for serializing params unless the messages go
|
||||
// somewhere.
|
||||
if (this._log.level <= Log4Moz.Level.Trace) {
|
||||
let msg = "Stmt #" + index + " " + sql;
|
||||
|
||||
if (params) {
|
||||
msg += " - " + JSON.stringify(params);
|
||||
}
|
||||
this._log.trace(msg);
|
||||
} else {
|
||||
this._log.debug("Stmt #" + index + " starting");
|
||||
}
|
||||
|
||||
let self = this;
|
||||
let pending = statement.executeAsync({
|
||||
handleResult: function (resultSet) {
|
||||
// .cancel() may not be immediate and handleResult() could be called
|
||||
// after a .cancel().
|
||||
for (let row = resultSet.getNextRow(); row && !userCancelled; row = resultSet.getNextRow()) {
|
||||
if (!onRow) {
|
||||
rows.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
onRow(row);
|
||||
} catch (e if e instanceof StopIteration) {
|
||||
userCancelled = true;
|
||||
pending.cancel();
|
||||
break;
|
||||
} catch (ex) {
|
||||
self._log.warn("Exception when calling onRow callback: " +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleError: function (error) {
|
||||
self._log.info("Error when executing SQL (" + error.result + "): " +
|
||||
error.message);
|
||||
errors.push(error);
|
||||
},
|
||||
|
||||
handleCompletion: function (reason) {
|
||||
self._log.debug("Stmt #" + index + " finished");
|
||||
self._inProgressStatements.delete(index);
|
||||
|
||||
switch (reason) {
|
||||
case Ci.mozIStorageStatementCallback.REASON_FINISHED:
|
||||
// If there is an onRow handler, we always resolve to null.
|
||||
let result = onRow ? null : rows;
|
||||
deferred.resolve(result);
|
||||
break;
|
||||
|
||||
case Ci.mozIStorageStatementCallback.REASON_CANCELLED:
|
||||
// It is not an error if the user explicitly requested cancel via
|
||||
// the onRow handler.
|
||||
if (userCancelled) {
|
||||
let result = onRow ? null : rows;
|
||||
deferred.resolve(result);
|
||||
} else {
|
||||
deferred.reject(new Error("Statement was cancelled."));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Ci.mozIStorageStatementCallback.REASON_ERROR:
|
||||
let error = new Error("Error(s) encountered during statement execution.");
|
||||
error.errors = errors;
|
||||
deferred.reject(error);
|
||||
break;
|
||||
|
||||
default:
|
||||
deferred.reject(new Error("Unknown completion reason code: " +
|
||||
reason));
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this._inProgressStatements.set(index, pending);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
_ensureOpen: function () {
|
||||
if (!this._open) {
|
||||
throw new Error("Connection is not open.");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.Sqlite = {
|
||||
openConnection: openConnection,
|
||||
};
|
16
toolkit/modules/tests/Makefile.in
Normal file
16
toolkit/modules/tests/Makefile.in
Normal file
@ -0,0 +1,16 @@
|
||||
# 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/.
|
||||
|
||||
DEPTH = @DEPTH@
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
relativesrcdir = @relativesrcdir@
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
XPCSHELL_TESTS = xpcshell
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
||||
|
259
toolkit/modules/tests/xpcshell/test_sqlite.js
Normal file
259
toolkit/modules/tests/xpcshell/test_sqlite.js
Normal file
@ -0,0 +1,259 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
do_get_profile();
|
||||
|
||||
Cu.import("resource://gre/modules/commonjs/promise/core.js");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Sqlite.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
|
||||
function getConnection(dbName) {
|
||||
let path = dbName + ".sqlite";
|
||||
|
||||
return Sqlite.openConnection({path: path});
|
||||
}
|
||||
|
||||
function getDummyDatabase(name) {
|
||||
const TABLES = {
|
||||
dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT",
|
||||
files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT",
|
||||
};
|
||||
|
||||
let c = yield getConnection(name);
|
||||
|
||||
for (let [k, v] in Iterator(TABLES)) {
|
||||
yield c.execute("CREATE TABLE " + k + "(" + v + ")");
|
||||
}
|
||||
|
||||
throw new Task.Result(c);
|
||||
}
|
||||
|
||||
|
||||
function run_test() {
|
||||
Cu.import("resource://testing-common/services-common/logging.js");
|
||||
initTestLogging("Trace");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test_open_normal() {
|
||||
let c = yield Sqlite.openConnection({path: "test_open_normal.sqlite"});
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_open_unshared() {
|
||||
let path = OS.Path.join(OS.Constants.Path.profileDir, "test_open_unshared.sqlite");
|
||||
|
||||
let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false});
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_get_dummy_database() {
|
||||
let db = yield getDummyDatabase("get_dummy_database");
|
||||
|
||||
do_check_eq(typeof(db), "object");
|
||||
yield db.close();
|
||||
});
|
||||
|
||||
add_task(function test_simple_insert() {
|
||||
let c = yield getDummyDatabase("simple_insert");
|
||||
|
||||
let result = yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')");
|
||||
do_check_true(Array.isArray(result));
|
||||
do_check_eq(result.length, 0);
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_simple_bound_array() {
|
||||
let c = yield getDummyDatabase("simple_bound_array");
|
||||
|
||||
let result = yield c.execute("INSERT INTO dirs VALUES (?, ?)", [1, "foo"]);
|
||||
do_check_eq(result.length, 0);
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_simple_bound_object() {
|
||||
let c = yield getDummyDatabase("simple_bound_object");
|
||||
let result = yield c.execute("INSERT INTO dirs VALUES (:id, :path)",
|
||||
{id: 1, path: "foo"});
|
||||
do_check_eq(result.length, 0);
|
||||
do_check_eq(c.lastInsertRowID, 1);
|
||||
do_check_eq(c.affectedRows, 1);
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
// This is mostly a sanity test to ensure simple executions work.
|
||||
add_task(function test_simple_insert_then_select() {
|
||||
let c = yield getDummyDatabase("simple_insert_then_select");
|
||||
|
||||
yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')");
|
||||
yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["bar"]);
|
||||
|
||||
let result = yield c.execute("SELECT * FROM dirs");
|
||||
do_check_eq(result.length, 2);
|
||||
|
||||
let i = 0;
|
||||
for (let row of result) {
|
||||
i++;
|
||||
|
||||
do_check_eq(row.numEntries, 2);
|
||||
do_check_eq(row.getResultByIndex(0), i);
|
||||
|
||||
let expected = {1: "foo", 2: "bar"}[i];
|
||||
do_check_eq(row.getResultByName("path"), expected);
|
||||
}
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_repeat_execution() {
|
||||
let c = yield getDummyDatabase("repeat_execution");
|
||||
|
||||
let sql = "INSERT INTO dirs (path) VALUES (:path)";
|
||||
yield c.executeCached(sql, {path: "foo"});
|
||||
yield c.executeCached(sql);
|
||||
|
||||
let result = yield c.execute("SELECT * FROM dirs");
|
||||
|
||||
do_check_eq(result.length, 2);
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_table_exists() {
|
||||
let c = yield getDummyDatabase("table_exists");
|
||||
|
||||
do_check_false(yield c.tableExists("does_not_exist"));
|
||||
do_check_true(yield c.tableExists("dirs"));
|
||||
do_check_true(yield c.tableExists("files"));
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_index_exists() {
|
||||
let c = yield getDummyDatabase("index_exists");
|
||||
|
||||
do_check_false(yield c.indexExists("does_not_exist"));
|
||||
|
||||
yield c.execute("CREATE INDEX my_index ON dirs (path)");
|
||||
do_check_true(yield c.indexExists("my_index"));
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_close_cached() {
|
||||
let c = yield getDummyDatabase("close_cached");
|
||||
|
||||
yield c.executeCached("SELECT * FROM dirs");
|
||||
yield c.executeCached("SELECT * FROM files");
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_execute_invalid_statement() {
|
||||
let c = yield getDummyDatabase("invalid_statement");
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
c.execute("SELECT invalid FROM unknown").then(do_throw, function onError(error) {
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
yield deferred.promise;
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_on_row_exception_ignored() {
|
||||
let c = yield getDummyDatabase("on_row_exception_ignored");
|
||||
|
||||
let sql = "INSERT INTO dirs (path) VALUES (?)";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
yield c.executeCached(sql, ["dir" + i]);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
yield c.execute("SELECT * FROM DIRS", null, function onRow(row) {
|
||||
i++;
|
||||
|
||||
throw new Error("Some silly error.");
|
||||
});
|
||||
|
||||
do_check_eq(i, 10);
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
// Ensure StopIteration during onRow causes processing to stop.
|
||||
add_task(function test_on_row_stop_iteration() {
|
||||
let c = yield getDummyDatabase("on_row_stop_iteration");
|
||||
|
||||
let sql = "INSERT INTO dirs (path) VALUES (?)";
|
||||
for (let i = 0; i < 10; i++) {
|
||||
yield c.executeCached(sql, ["dir" + i]);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let result = yield c.execute("SELECT * FROM dirs", null, function onRow(row) {
|
||||
i++;
|
||||
|
||||
if (i == 5) {
|
||||
throw StopIteration;
|
||||
}
|
||||
});
|
||||
|
||||
do_check_null(result);
|
||||
do_check_eq(i, 5);
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_execute_transaction_success() {
|
||||
let c = yield getDummyDatabase("execute_transaction_success");
|
||||
|
||||
do_check_false(c.transactionInProgress);
|
||||
|
||||
yield c.executeTransaction(function transaction(conn) {
|
||||
do_check_eq(c, conn);
|
||||
do_check_true(conn.transactionInProgress);
|
||||
|
||||
yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')");
|
||||
});
|
||||
|
||||
do_check_false(c.transactionInProgress);
|
||||
let rows = yield c.execute("SELECT * FROM dirs");
|
||||
do_check_true(Array.isArray(rows));
|
||||
do_check_eq(rows.length, 1);
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
||||
add_task(function test_execute_transaction_rollback() {
|
||||
let c = yield getDummyDatabase("execute_transaction_rollback");
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
c.executeTransaction(function transaction(conn) {
|
||||
yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')");
|
||||
print("Expecting error with next statement.");
|
||||
yield conn.execute("INSERT INTO invalid VALUES ('foo')");
|
||||
|
||||
// We should never get here.
|
||||
do_throw();
|
||||
}).then(do_throw, function onError(error) {
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
yield deferred.promise;
|
||||
|
||||
let rows = yield c.execute("SELECT * FROM dirs");
|
||||
do_check_eq(rows.length, 0);
|
||||
|
||||
yield c.close();
|
||||
});
|
||||
|
5
toolkit/modules/tests/xpcshell/xpcshell.ini
Normal file
5
toolkit/modules/tests/xpcshell/xpcshell.ini
Normal file
@ -0,0 +1,5 @@
|
||||
[DEFAULT]
|
||||
head =
|
||||
tail =
|
||||
|
||||
[test_sqlite.js]
|
@ -491,6 +491,8 @@ MAKEFILES_xulapp="
|
||||
toolkit/forgetaboutsite/test/browser/Makefile
|
||||
toolkit/identity/Makefile
|
||||
toolkit/locales/Makefile
|
||||
toolkit/modules/Makefile
|
||||
toolkit/modules/tests/Makefile
|
||||
toolkit/mozapps/downloads/Makefile
|
||||
toolkit/mozapps/extensions/Makefile
|
||||
toolkit/mozapps/handling/Makefile
|
||||
|
Loading…
Reference in New Issue
Block a user