Bug 458811 - Allow for multiple statements to be executed at in a transaction asynchronously

This changeset allows consumers to execute a series of statements, in order,
asynchronously in a transaction.
r=dcamp
sr=sicking
This commit is contained in:
Shawn Wilsher 2008-10-13 18:45:40 -04:00
Родитель 135aace591
Коммит de4ea794f3
7 изменённых файлов: 402 добавлений и 112 удалений

Просмотреть файл

@ -45,6 +45,8 @@ interface mozIStorageAggregateFunction;
interface mozIStorageFunction;
interface mozIStorageProgressHandler;
interface mozIStorageStatement;
interface mozIStorageStatementCallback;
interface mozIStoragePendingStatement;
interface nsIFile;
/**
@ -56,12 +58,12 @@ interface nsIFile;
*
* @threadsafe
*/
[scriptable, uuid(623f9ddb-434b-4d39-bc2d-1c617da241d0)]
[scriptable, uuid(ac3c486c-69a1-4cbe-8f25-2ad20880eab3)]
interface mozIStorageConnection : nsISupports {
/*
* Initialization and status
*/
/**
* Closes a database connection. C++ callers should simply set the database
* variable to NULL.
@ -126,6 +128,31 @@ interface mozIStorageConnection : nsISupports {
*/
void executeSimpleSQL(in AUTF8String aSQLStatement);
/**
* Execute an array of queries created with this connection asynchronously
* using any currently bound parameters. The statements are ran wrapped in a
* transaction. These statements can be reused immediately, and reset does
* not need to be called.
*
* Note: If you have any custom defined functions, they must be re-entrant
* since they can be called on multiple threads.
*
* @param aStatements
* The array of statements to execute asynchronously, in the order they
* are given in the array.
* @param aNumStatements
* The number of statements in aStatements.
* @param aCallback [optional]
* The callback object that will be notified of progress, errors, and
* completion.
* @returns an object that can be used to cancel the statements execution.
*/
mozIStoragePendingStatement executeAsync(
[array, size_is(aNumStatements)] in mozIStorageStatement aStatements,
in unsigned long aNumStatements,
[optional] in mozIStorageStatementCallback aCallback
);
/**
* Check if the given table exists.
*

Просмотреть файл

@ -40,6 +40,7 @@
#define _MOZSTORAGEHELPER_H_
#include "mozIStorageConnection.h"
#include "mozIStorageStatement.h"
/**

Просмотреть файл

@ -54,6 +54,7 @@
#include "mozIStorageAggregateFunction.h"
#include "mozIStorageFunction.h"
#include "mozStorageEvents.h"
#include "mozStorageUnicodeFunctions.h"
#include "mozStorageConnection.h"
#include "mozStorageService.h"
@ -358,6 +359,60 @@ mozStorageConnection::ExecuteSimpleSQL(const nsACString& aSQLStatement)
return NS_OK;
}
nsresult
mozStorageConnection::ExecuteAsync(mozIStorageStatement ** aStatements,
PRUint32 aNumStatements,
mozIStorageStatementCallback *aCallback,
mozIStoragePendingStatement **_stmt)
{
int rc = SQLITE_OK;
nsTArray<sqlite3_stmt *> stmts(aNumStatements);
for (PRUint32 i = 0; i < aNumStatements && rc == SQLITE_OK; i++) {
sqlite3_stmt *old_stmt = aStatements[i]->GetNativeStatementPointer();
NS_ASSERTION(sqlite3_db_handle(old_stmt) == mDBConn,
"Statement must be from this database connection!");
// Clone this statement. We only need a sqlite3_stmt object, so we can
// avoid all the extra work that making a new mozStorageStatement would
// normally involve and use the SQLite API.
sqlite3_stmt *new_stmt;
rc = sqlite3_prepare_v2(mDBConn, sqlite3_sql(old_stmt), -1, &new_stmt,
NULL);
if (rc != SQLITE_OK)
break;
// Transfer the bindings
rc = sqlite3_transfer_bindings(old_stmt, new_stmt);
if (rc != SQLITE_OK)
break;
if (!stmts.AppendElement(new_stmt)) {
rc = SQLITE_NOMEM;
break;
}
}
// Dispatch to the background
nsresult rv = NS_OK;
if (rc == SQLITE_OK)
rv = NS_executeAsync(stmts, this, aCallback, _stmt);
// We had a failure, so we need to clean up...
if (rc != SQLITE_OK || NS_FAILED(rv)) {
for (PRUint32 i = 0; i < stmts.Length(); i++)
(void)sqlite3_finalize(stmts[i]);
if (rc != SQLITE_OK)
rv = ConvertResultCode(rc);
}
// Always reset all the statements
for (PRUint32 i = 0; i < aNumStatements; i++)
(void)aStatements[i]->Reset();
return rv;
}
NS_IMETHODIMP
mozStorageConnection::TableExists(const nsACString& aSQLStatement, PRBool *_retval)
{

Просмотреть файл

@ -46,6 +46,7 @@
#include "mozIStorageStatementCallback.h"
#include "mozIStoragePendingStatement.h"
#include "mozStorageHelper.h"
#include "mozStorageResultSet.h"
#include "mozStorageRow.h"
#include "mozStorageBackground.h"
@ -272,15 +273,19 @@ public:
/**
* This takes ownership of both the statement and the callback.
*/
AsyncExecute(sqlite3_stmt *aStatement,
AsyncExecute(nsTArray<sqlite3_stmt *> &aStatements,
mozIStorageConnection *aConnection,
mozIStorageStatementCallback *aCallback) :
mStatement(aStatement)
mConnection(aConnection)
, mTransactionManager(nsnull)
, mCallback(aCallback)
, mCallingThread(do_GetCurrentThread())
, mState(PENDING)
, mStateMutex(nsAutoLock::NewLock("AsyncExecute::mStateMutex"))
, mPendingEventsMutex(nsAutoLock::NewLock("AsyncExecute::mPendingEventsMutex"))
{
(void)mStatements.SwapElements(aStatements);
NS_ASSERTION(mStatements.Length(), "We weren't given any statements!");
}
nsresult initialize()
@ -300,94 +305,106 @@ public:
return Complete();
}
// Execute the statement, giving the callback results
// XXX better chunking of results?
nsresult rv = NS_OK;
while (PR_TRUE) {
int rc = sqlite3_step(mStatement);
// Break out if we have no more results
if (rc == SQLITE_DONE)
break;
// If there is more than one statement, run it in a transaction. We assume
// that we have been given write statements since getting a batch of read
// statements doesn't make a whole lot of sense.
if (mStatements.Length() > 1) {
// We don't error if this failed because it's not terrible if it does.
mTransactionManager = new mozStorageTransaction(mConnection, PR_FALSE,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
}
// Some errors are not fatal, and we can handle them and continue.
if (rc != SQLITE_OK && rc != SQLITE_ROW) {
if (rc == SQLITE_BUSY) {
// Yield, and try again
PR_Sleep(PR_INTERVAL_NO_WAIT);
continue;
// Execute each statement, giving the callback results if it returns any.
nsresult rv = NS_OK;
for (PRUint32 i = 0; i < mStatements.Length(); i++) {
while (PR_TRUE) {
int rc = sqlite3_step(mStatements[i]);
// Break out if we have no more results
if (rc == SQLITE_DONE)
break;
// Some errors are not fatal, and we can handle them and continue.
if (rc != SQLITE_OK && rc != SQLITE_ROW) {
if (rc == SQLITE_BUSY) {
// Yield, and try again
PR_Sleep(PR_INTERVAL_NO_WAIT);
continue;
}
// Set error state
{
nsAutoLock mutex(mStateMutex);
mState = ERROR;
}
// Notify
sqlite3 *db = sqlite3_db_handle(mStatements[i]);
iCancelable *cancelable = ErrorNotifier::Dispatch(
mCallingThread, mCallback, this, rc, sqlite3_errmsg(db)
);
if (cancelable) {
nsAutoLock mutex(mPendingEventsMutex);
(void)mPendingEvents.AppendObject(cancelable);
}
// And complete
return Complete();
}
// Set error state
// Check to see if we have been canceled
{
nsAutoLock mutex(mStateMutex);
mState = ERROR;
if (mState == CANCELED)
return Complete();
}
// Notify
sqlite3 *db = sqlite3_db_handle(mStatement);
iCancelable *cancelable = ErrorNotifier::Dispatch(
mCallingThread, mCallback, this, rc, sqlite3_errmsg(db)
);
if (cancelable) {
// If we do not have a callback, there's no point in executing this
// statement anymore.
if (!mCallback)
break;
// Build result object
// XXX bug 454740 chunk these results better
nsRefPtr<mozStorageResultSet> results(new mozStorageResultSet());
if (!results) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
nsRefPtr<mozStorageRow> row(new mozStorageRow());
if (!row) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
rv = row->initialize(mStatements[i]);
if (NS_FAILED(rv)) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
rv = results->add(row);
if (NS_FAILED(rv))
break;
// Notify caller
nsRefPtr<CallbackResultNotifier> notifier =
new CallbackResultNotifier(mCallback, results, this);
if (!notifier) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
nsresult status = mCallingThread->Dispatch(notifier, NS_DISPATCH_NORMAL);
if (NS_SUCCEEDED(status)) {
nsAutoLock mutex(mPendingEventsMutex);
(void)mPendingEvents.AppendObject(cancelable);
(void)mPendingEvents.AppendObject(notifier);
}
// And complete
return Complete();
}
// Check to see if we have been canceled
{
nsAutoLock mutex(mStateMutex);
if (mState == CANCELED)
return Complete();
}
// If we do not have a callback, but are getting results, we should stop
// now since all this work isn't going to accomplish anything
if (!mCallback) {
nsAutoLock mutex(mStateMutex);
mState = COMPLETED;
return Complete();
}
// Build result object
nsRefPtr<mozStorageResultSet> results(new mozStorageResultSet());
if (!results) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
nsRefPtr<mozStorageRow> row(new mozStorageRow());
if (!row) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
rv = row->initialize(mStatement);
if (NS_FAILED(rv)) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
rv = results->add(row);
// If we have an error, we need to break out now.
if (NS_FAILED(rv))
break;
// Notify caller
nsRefPtr<CallbackResultNotifier> notifier =
new CallbackResultNotifier(mCallback, results, this);
if (!notifier) {
rv = NS_ERROR_OUT_OF_MEMORY;
break;
}
nsresult status = mCallingThread->Dispatch(notifier, NS_DISPATCH_NORMAL);
if (NS_SUCCEEDED(status)) {
nsAutoLock mutex(mPendingEventsMutex);
(void)mPendingEvents.AppendObject(notifier);
}
}
// We have broken out of the loop because of an error or because we are
@ -474,9 +491,33 @@ private:
NS_ASSERTION(mState != PENDING,
"Still in a pending state when calling Complete!");
// Reset the statement
(void)sqlite3_finalize(mStatement);
mStatement = NULL;
// Handle our transaction, if we have one
if (mTransactionManager) {
if (mState == COMPLETED) {
nsresult rv = mTransactionManager->Commit();
if (NS_FAILED(rv)) {
iCancelable *cancelable = ErrorNotifier::Dispatch(
mCallingThread, mCallback, this, mozIStorageError::ERROR,
"Transaction failed to commit"
);
if (cancelable) {
nsAutoLock mutex(mPendingEventsMutex);
(void)mPendingEvents.AppendObject(cancelable);
}
}
}
else {
(void)mTransactionManager->Rollback();
}
delete mTransactionManager;
mTransactionManager = nsnull;
}
// Finalize our statements
for (PRUint32 i = 0; i < mStatements.Length(); i++) {
(void)sqlite3_finalize(mStatements[i]);
mStatements[i] = NULL;
}
// Notify about completion iff we have a callback.
if (mCallback) {
@ -495,7 +536,9 @@ private:
return NS_OK;
}
sqlite3_stmt *mStatement;
nsTArray<sqlite3_stmt *> mStatements;
mozIStorageConnection *mConnection;
mozStorageTransaction *mTransactionManager;
mozIStorageStatementCallback *mCallback;
nsCOMPtr<nsIThread> mCallingThread;
@ -527,12 +570,13 @@ NS_IMPL_THREADSAFE_ISUPPORTS2(
)
nsresult
NS_executeAsync(sqlite3_stmt *aStatement,
NS_executeAsync(nsTArray<sqlite3_stmt *> &aStatements,
mozIStorageConnection *aConnection,
mozIStorageStatementCallback *aCallback,
mozIStoragePendingStatement **_stmt)
{
// Create our event to run in the background
nsRefPtr<AsyncExecute> event(new AsyncExecute(aStatement, aCallback));
nsRefPtr<AsyncExecute> event(new AsyncExecute(aStatements, aConnection, aCallback));
NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY);
nsresult rv = event->initialize();

Просмотреть файл

@ -41,6 +41,7 @@
#define _mozStorageEvents_h_
#include "nscore.h"
#include "nsTArray.h"
#include "mozStorageBackground.h"
struct sqlite3_stmt;
class mozIStorageStatementCallback;
@ -50,15 +51,18 @@ class mozIStoragePendingStatement;
* Executes a statement in the background, and passes results back to the
* caller.
*
* @param aStatement
* The SQLite statement to execute in the background.
* @param aStatements
* The SQLite statements to execute in the background.
* @param aConnection
* The connection that created the statements to execute.
* @param aCallback
* The callback that is notified of results, completion, and errors.
* @param _stmt
* The handle to control the execution of the statement.
* The handle to control the execution of the statements.
*/
nsresult NS_executeAsync(
sqlite3_stmt *aStatement,
nsTArray<sqlite3_stmt *> &aStatements,
mozIStorageConnection *aConnection,
mozIStorageStatementCallback *aCallback,
mozIStoragePendingStatement **_stmt
);

Просмотреть файл

@ -578,29 +578,8 @@ nsresult
mozStorageStatement::ExecuteAsync(mozIStorageStatementCallback *aCallback,
mozIStoragePendingStatement **_stmt)
{
// Clone this statement. We only need a sqlite3_stmt object, so we can
// avoid all the extra work that making a new mozStorageStatement would
// normally involve and use the SQLite API.
sqlite3 *db = mDBConnection->GetNativeConnection();
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(db, sqlite3_sql(mDBStatement), -1, &stmt, NULL);
if (rc != SQLITE_OK)
return ConvertResultCode(rc);
// Transfer the bindings
rc = sqlite3_transfer_bindings(mDBStatement, stmt);
if (rc != SQLITE_OK)
return ConvertResultCode(rc);
// Dispatch to the background.
nsresult rv = NS_executeAsync(stmt, aCallback, _stmt);
NS_ENSURE_SUCCESS(rv, rv);
// Reset this statement.
rv = Reset();
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
mozIStorageStatement * stmts[1] = {this};
return mDBConnection->ExecuteAsync(stmts, 1, aCallback, _stmt);
}
/* [noscript,notxpcom] sqlite3stmtptr getNativeStatementPointer(); */

Просмотреть файл

@ -0,0 +1,180 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Storage Test Code.
*
* The Initial Developer of the Original Code is
* Mozilla Corporation.
* Portions created by the Initial Developer are Copyright (C) 2008
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Shawn Wilsher <me@shawnwilsher.com> (Original Author)
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
// This file tests the functionality of mozIStorageConnection::executeAsync
const INTEGER = 1;
const TEXT = "this is test text";
const REAL = 3.23;
const BLOB = [1, 2];
function test_create_and_add()
{
dump("test_create_and_add()\n");
getOpenedDatabase().executeSimpleSQL(
"CREATE TABLE test (" +
"id INTEGER PRIMARY KEY, " +
"string TEXT, " +
"number REAL, " +
"nuller NULL, " +
"blober BLOB" +
")"
);
let stmts = [];
stmts[0] = getOpenedDatabase().createStatement(
"INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)"
);
stmts[0].bindInt32Parameter(0, INTEGER);
stmts[0].bindStringParameter(1, TEXT);
stmts[0].bindDoubleParameter(2, REAL);
stmts[0].bindNullParameter(3);
stmts[0].bindBlobParameter(4, BLOB, BLOB.length);
stmts[1] = getOpenedDatabase().createStatement(
"INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)"
);
stmts[1].bindStringParameter(0, TEXT);
stmts[1].bindDoubleParameter(1, REAL);
stmts[1].bindNullParameter(2);
stmts[1].bindBlobParameter(3, BLOB, BLOB.length);
do_test_pending();
getOpenedDatabase().executeAsync(stmts, stmts.length, {
handleResult: function(aResultSet)
{
dump("handleResult("+aResultSet+")\n");
do_throw("unexpected results obtained!");
},
handleError: function(aError)
{
dump("handleError("+aError.result+")\n");
do_throw("unexpected error!");
},
handleCompletion: function(aReason)
{
dump("handleCompletion("+aReason+")\n");
do_check_eq(Ci.mozIStorageStatementCallback.REASON_FINISHED, aReason);
// Check that the result is in the table
let stmt = getOpenedDatabase().createStatement(
"SELECT string, number, nuller, blober FROM test WHERE id = ?"
);
stmt.bindInt32Parameter(0, INTEGER);
try {
do_check_true(stmt.executeStep());
do_check_eq(TEXT, stmt.getString(0));
do_check_eq(REAL, stmt.getDouble(1));
do_check_true(stmt.getIsNull(2));
let count = { value: 0 };
let blob = { value: null };
stmt.getBlob(3, count, blob);
do_check_eq(BLOB.length, count.value);
for (let i = 0; i < BLOB.length; i++)
do_check_eq(BLOB[i], blob.value[i]);
}
finally {
stmt.finalize();
}
// Make sure we have two rows in the table
stmt = getOpenedDatabase().createStatement(
"SELECT COUNT(1) FROM test"
);
try {
do_check_true(stmt.executeStep());
do_check_eq(2, stmt.getInt32(0));
}
finally {
stmt.finalize();
}
do_test_finished();
}
});
stmts[0].finalize();
stmts[1].finalize();
}
function test_transaction_created()
{
dump("test_transaction_created()\n");
let stmts = [];
stmts[0] = getOpenedDatabase().createStatement(
"BEGIN"
);
stmts[1] = getOpenedDatabase().createStatement(
"SELECT * FROM test"
);
do_test_pending()
getOpenedDatabase().executeAsync(stmts, stmts.length, {
handleResult: function(aResultSet)
{
dump("handleResults("+aResultSet+")\n");
do_throw("unexpected results obtained!");
},
handleError: function(aError)
{
dump("handleError("+aError.result+")\n");
},
handleCompletion: function(aReason)
{
dump("handleCompletion("+aReason+")\n");
do_check_eq(Ci.mozIStorageStatementCallback.REASON_ERROR, aReason);
do_test_finished();
}
});
stmts[0].finalize();
stmts[1].finalize();
}
let tests =
[
test_create_and_add,
test_transaction_created,
];
function run_test()
{
cleanup();
for (let i = 0; i < tests.length; i++)
tests[i]();
}