Bug 583882 - Need a way to clone an existing connection.
authorShawn Wilsher <sdwilsh@shawnwilsher.com>
Fri, 27 Aug 2010 12:42:58 -0700
changeset 51598 bc95b9869d3f593d0b4e6e938075c15cd8a5db9c
parent 51597 88be5af389ed77a857a3ad338fed7fcbaaadd628
child 51599 0b09b81b1a608a33a861b46df5e22ab4536473c7
push id15366
push usersdwilsh@shawnwilsher.com
push dateFri, 27 Aug 2010 19:47:54 +0000
treeherdermozilla-central@bc95b9869d3f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs583882
milestone2.0b5pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 583882 - Need a way to clone an existing connection. r=asuth sr=shaver a=blocking2.0
storage/public/mozIStorageConnection.idl
storage/src/mozStorageBindingParams.h
storage/src/mozStorageConnection.cpp
storage/src/mozStorageConnection.h
storage/src/mozStorageService.cpp
storage/test/unit/test_storage_connection.js
--- a/storage/public/mozIStorageConnection.idl
+++ b/storage/public/mozIStorageConnection.idl
@@ -56,21 +56,18 @@ interface nsIFile;
  * mozIStorageConnection represents a database connection attached to
  * a specific file or to the in-memory data storage.  It is the
  * primary interface for interacting with a database, including
  * creating prepared statements, executing SQL, and examining database
  * errors.
  *
  * @threadsafe
  */
-[scriptable, uuid(32b43ec6-e905-4070-9cfe-370c45f7c1bf)]
+[scriptable, uuid(7b5ed315-58b3-41fd-8257-d5768943021d)]
 interface mozIStorageConnection : nsISupports {
-  //////////////////////////////////////////////////////////////////////////////
-  //// Initialization and status
-
   /**
    * Closes a database connection.  Callers must finalize all statements created
    * for this connection prior to calling this method.  It is illegal to use
    * call this method if any asynchronous statements have been executed on this
    * connection.
    *
    * @throws NS_ERROR_UNEXPECTED
    *         If any statement has been executed asynchronously on this object.
@@ -87,16 +84,35 @@ interface mozIStorageConnection : nsISup
    *        A callback that will be notified when the close is completed.
    *
    * @throws NS_ERROR_UNEXPECTED
    *         If is called on a thread other than the one that opened it.
    */
   void asyncClose([optional] in mozIStorageCompletionCallback aCallback);
 
   /**
+   * Clones a database and makes the clone read only if needed.
+   *
+   * @note If your connection is already read-only, you will get a read-only
+   *       clone.
+   * @note Due to a bug in SQLite, if you use the shared cache (openDatabase),
+   *       you end up with the same privileges as the first connection opened
+   *       regardless of what is specified in aReadOnly.
+   *
+   * @throws NS_ERROR_UNEXPECTED
+   *         If this connection is a memory database.
+   *
+   * @param aReadOnly
+   *        If true, the returned database should be put into read-only mode.
+   *        Defaults to false.
+   * @return the cloned database connection.
+   */
+  mozIStorageConnection clone([optional] in boolean aReadOnly);
+
+  /**
    * Indicates if the connection is open and ready to use.  This will be false
    * if the connection failed to open, or it has been closed.
    */
   readonly attribute boolean connectionReady;
 
   /**
    * The current database nsIFile.  Null if the database
    * connection refers to an in-memory database.
--- a/storage/src/mozStorageBindingParams.h
+++ b/storage/src/mozStorageBindingParams.h
@@ -38,17 +38,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef mozStorageBindingParams_h
 #define mozStorageBindingParams_h
 
 #include "nsCOMArray.h"
 #include "nsIVariant.h"
-#include "nsTHashtable.h"
+#include "nsInterfaceHashtable.h"
 
 #include "mozStorageBindingParamsArray.h"
 #include "mozStorageStatement.h"
 #include "mozStorageAsyncStatement.h"
 
 #include "mozIStorageBindingParams.h"
 #include "IStorageBindingParamsInternal.h"
 
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -162,27 +162,51 @@ void tracefunc (void *aClosure, const ch
 
 struct FFEArguments
 {
     nsISupports *target;
     bool found;
 };
 PLDHashOperator
 findFunctionEnumerator(const nsACString &aKey,
-                       nsISupports *aData,
+                       Connection::FunctionInfo aData,
                        void *aUserArg)
 {
   FFEArguments *args = static_cast<FFEArguments *>(aUserArg);
-  if (aData == args->target) {
-    args->found = PR_TRUE;
+  if (aData.function == args->target) {
+    args->found = true;
     return PL_DHASH_STOP;
   }
   return PL_DHASH_NEXT;
 }
 
+PLDHashOperator
+copyFunctionEnumerator(const nsACString &aKey,
+                       Connection::FunctionInfo aData,
+                       void *aUserArg)
+{
+  NS_PRECONDITION(aData.type == Connection::FunctionInfo::SIMPLE ||
+                  aData.type == Connection::FunctionInfo::AGGREGATE,
+                  "Invalid function type!");
+
+  Connection *connection = static_cast<Connection *>(aUserArg);
+  if (aData.type == Connection::FunctionInfo::SIMPLE) {
+    mozIStorageFunction *function =
+      static_cast<mozIStorageFunction *>(aData.function.get());
+    (void)connection->CreateFunction(aKey, aData.numArgs, function);
+  }
+  else {
+    mozIStorageAggregateFunction *function =
+      static_cast<mozIStorageAggregateFunction *>(aData.function.get());
+    (void)connection->CreateAggregateFunction(aKey, aData.numArgs, function);
+  }
+
+  return PL_DHASH_NEXT;
+}
+
 void
 basicFunctionHelper(sqlite3_context *aCtx,
                     int aArgc,
                     sqlite3_value **aArgv)
 {
   void *userData = ::sqlite3_user_data(aCtx);
 
   mozIStorageFunction *func = static_cast<mozIStorageFunction *>(userData);
@@ -303,24 +327,26 @@ private:
   nsCOMPtr<nsIRunnable> mCallbackEvent;
 };
 
 } // anonymous namespace
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Connection
 
-Connection::Connection(Service *aService)
+Connection::Connection(Service *aService,
+                       int aFlags)
 : sharedAsyncExecutionMutex("Connection::sharedAsyncExecutionMutex")
 , sharedDBMutex("Connection::sharedDBMutex")
 , threadOpenedOn(do_GetCurrentThread())
 , mDBConn(nsnull)
 , mAsyncExecutionThreadShuttingDown(false)
 , mTransactionInProgress(PR_FALSE)
 , mProgressHandler(nsnull)
+, mFlags(aFlags)
 , mStorageService(aService)
 {
   mFunctions.Init();
 }
 
 Connection::~Connection()
 {
   (void)Close();
@@ -348,40 +374,36 @@ Connection::getAsyncExecutionTarget()
       return nsnull;
     }
   }
 
   return mAsyncExecutionThread;
 }
 
 nsresult
-Connection::initialize(nsIFile *aDatabaseFile,
-                       int aFlags)
+Connection::initialize(nsIFile *aDatabaseFile)
 {
   NS_ASSERTION (!mDBConn, "Initialize called on already opened database!");
 
   int srv;
   nsresult rv;
 
   mDatabaseFile = aDatabaseFile;
 
-  // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility
-  // reasons.
-  int flags = aFlags | SQLITE_OPEN_CREATE;
   if (aDatabaseFile) {
     nsAutoString path;
     rv = aDatabaseFile->GetPath(path);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, flags,
+    srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, mFlags,
                             NULL);
   }
   else {
     // in memory database requested, sqlite uses a magic file name
-    srv = ::sqlite3_open_v2(":memory:", &mDBConn, flags, NULL);
+    srv = ::sqlite3_open_v2(":memory:", &mDBConn, mFlags, NULL);
   }
   if (srv != SQLITE_OK) {
     mDBConn = nsnull;
     return convertResultCode(srv);
   }
 
   // Properly wrap the database handle's mutex.
   sharedDBMutex.initWithMutex(sqlite3_db_mutex(mDBConn));
@@ -508,17 +530,17 @@ Connection::databaseElementExists(enum D
   return convertResultCode(srv);
 }
 
 bool
 Connection::findFunctionByInstance(nsISupports *aInstance)
 {
   sharedDBMutex.assertCurrentThreadOwns();
   FFEArguments args = { aInstance, false };
-  mFunctions.EnumerateRead(findFunctionEnumerator, &args);
+  (void)mFunctions.EnumerateRead(findFunctionEnumerator, &args);
   return args.found;
 }
 
 /* static */ int
 Connection::sProgressHelper(void *aArg)
 {
   Connection *_this = static_cast<Connection *>(aArg);
   return _this->progressHandler();
@@ -663,16 +685,44 @@ Connection::AsyncClose(mozIStorageComple
   NS_ENSURE_TRUE(closeEvent, NS_ERROR_OUT_OF_MEMORY);
 
   rv = asyncThread->Dispatch(closeEvent, NS_DISPATCH_NORMAL);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+Connection::Clone(PRBool aReadOnly,
+                  mozIStorageConnection **_connection)
+{
+  if (!mDBConn)
+    return NS_ERROR_NOT_INITIALIZED;
+  if (!mDatabaseFile)
+    return NS_ERROR_UNEXPECTED;
+
+  int flags = mFlags;
+  if (aReadOnly) {
+    // Turn off SQLITE_OPEN_READWRITE, and set SQLITE_OPEN_READONLY.
+    flags = (~SQLITE_OPEN_READWRITE & flags) | SQLITE_OPEN_READONLY;
+    // Turn off SQLITE_OPEN_CREATE.
+    flags = (~SQLITE_OPEN_CREATE & flags);
+  }
+  nsRefPtr<Connection> clone = new Connection(mStorageService, flags);
+  NS_ENSURE_TRUE(clone, NS_ERROR_OUT_OF_MEMORY);
+
+  nsresult rv = clone->initialize(mDatabaseFile);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Copy any functions that have been added to this connection.
+  (void)mFunctions.EnumerateRead(copyFunctionEnumerator, clone);
+
+  NS_ADDREF(*_connection = clone);
+  return NS_OK;
+}
 
 NS_IMETHODIMP
 Connection::GetConnectionReady(PRBool *_ready)
 {
   *_ready = (mDBConn != nsnull);
   return NS_OK;
 }
 
@@ -945,17 +995,20 @@ Connection::CreateFunction(const nsACStr
                                       SQLITE_ANY,
                                       aFunction,
                                       basicFunctionHelper,
                                       NULL,
                                       NULL);
   if (srv != SQLITE_OK)
     return convertResultCode(srv);
 
-  NS_ENSURE_TRUE(mFunctions.Put(aFunctionName, aFunction),
+  FunctionInfo info = { aFunction,
+                        Connection::FunctionInfo::SIMPLE,
+                        aNumArguments };
+  NS_ENSURE_TRUE(mFunctions.Put(aFunctionName, info),
                  NS_ERROR_OUT_OF_MEMORY);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 Connection::CreateAggregateFunction(const nsACString &aFunctionName,
                                     PRInt32 aNumArguments,
@@ -978,17 +1031,20 @@ Connection::CreateAggregateFunction(cons
                                       SQLITE_ANY,
                                       aFunction,
                                       NULL,
                                       aggregateFunctionStepHelper,
                                       aggregateFunctionFinalHelper);
   if (srv != SQLITE_OK)
     return convertResultCode(srv);
 
-  NS_ENSURE_TRUE(mFunctions.Put(aFunctionName, aFunction),
+  FunctionInfo info = { aFunction,
+                        Connection::FunctionInfo::AGGREGATE,
+                        aNumArguments };
+  NS_ENSURE_TRUE(mFunctions.Put(aFunctionName, info),
                  NS_ERROR_OUT_OF_MEMORY);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 Connection::RemoveFunction(const nsACString &aFunctionName)
 {
--- a/storage/src/mozStorageConnection.h
+++ b/storage/src/mozStorageConnection.h
@@ -41,17 +41,17 @@
 #ifndef MOZSTORAGECONNECTION_H
 #define MOZSTORAGECONNECTION_H
 
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "mozilla/Mutex.h"
 
 #include "nsString.h"
-#include "nsInterfaceHashtable.h"
+#include "nsDataHashtable.h"
 #include "mozIStorageProgressHandler.h"
 #include "SQLiteMutex.h"
 #include "mozIStorageConnection.h"
 #include "mozStorageService.h"
 
 #include "nsIMutableArray.h"
 
 #include "sqlite3.h"
@@ -65,30 +65,48 @@ namespace mozilla {
 namespace storage {
 
 class Connection : public mozIStorageConnection
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_MOZISTORAGECONNECTION
 
-  Connection(Service *aService);
+  /**
+   * Structure used to describe user functions on the database connection.
+   */
+  struct FunctionInfo {
+    enum FunctionType {
+      SIMPLE,
+      AGGREGATE
+    };
+
+    nsCOMPtr<nsISupports> function;
+    FunctionType type;
+    PRInt32 numArgs;
+  };
+
+  /**
+   * @param aService
+   *        Pointer to the storage service.  Held onto for the lifetime of the
+   *        connection.
+   * @param aFlags
+   *        The flags to pass to sqlite3_open_v2.
+   */
+  Connection(Service *aService, int aFlags);
 
   /**
    * Creates the connection to the database.
    *
    * @param aDatabaseFile
    *        The nsIFile of the location of the database to open, or create if it
    *        does not exist.  Passing in nsnull here creates an in-memory
    *        database.
-   * @param aFlags
-   *        The flags to pass to sqlite3_open_v2.  For compatibility reasons,
-   *        We always pass SQLITE_OPEN_CREATE to sqlite3_open_v2.
    */
-  nsresult initialize(nsIFile *aDatabaseFile, int aFlags);
+  nsresult initialize(nsIFile *aDatabaseFile);
 
   // fetch the native handle
   sqlite3 *GetNativeConnection() { return mDBConn; }
 
   /**
    * Lazily creates and returns a background execution thread.  In the future,
    * the thread may be re-claimed if left idle, so you should call this
    * method just before you dispatch and not save the reference.
@@ -195,24 +213,29 @@ private:
    * mDBMutex.
    */
   PRBool mTransactionInProgress;
 
   /**
    * Stores the mapping of a given function by name to its instance.  Access is
    * protected by mDBMutex.
    */
-  nsInterfaceHashtable<nsCStringHashKey, nsISupports> mFunctions;
+  nsDataHashtable<nsCStringHashKey, FunctionInfo> mFunctions;
 
   /**
    * Stores the registered progress handler for the database connection.  Access
    * is protected by mDBMutex.
    */
   nsCOMPtr<mozIStorageProgressHandler> mProgressHandler;
 
+  /**
+   * Stores the flags we passed to sqlite3_open_v2.
+   */
+  const int mFlags;
+
   // This is here for two reasons: 1) It's used to make sure that the
   // connections do not outlive the service.  2) Our custom collating functions
   // call its localeCompareStrings() method.
   nsRefPtr<Service> mStorageService;
 };
 
 } // namespace storage
 } // namespace mozilla
--- a/storage/src/mozStorageService.cpp
+++ b/storage/src/mozStorageService.cpp
@@ -352,20 +352,20 @@ Service::OpenSpecialDatabase(const char 
     storageFile->GetPath(filename);
     nsCString filename8 = NS_ConvertUTF16toUTF8(filename.get());
     // fall through to DB initialization
   }
   else {
     return NS_ERROR_INVALID_ARG;
   }
 
-  Connection *msc = new Connection(this);
+  Connection *msc = new Connection(this, SQLITE_OPEN_READWRITE);
   NS_ENSURE_TRUE(msc, NS_ERROR_OUT_OF_MEMORY);
 
-  rv = msc->initialize(storageFile, SQLITE_OPEN_READWRITE);
+  rv = msc->initialize(storageFile);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NS_ADDREF(*_connection = msc);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 Service::OpenDatabase(nsIFile *aDatabaseFile,
@@ -374,21 +374,24 @@ Service::OpenDatabase(nsIFile *aDatabase
   NS_ENSURE_ARG(aDatabaseFile);
 
 #ifdef NS_FUNCTION_TIMER
   nsCString leafname;
   (void)aDatabaseFile->GetNativeLeafName(leafname);
   NS_TIME_FUNCTION_FMT("mozIStorageService::OpenDatabase(%s)", leafname.get());
 #endif
 
-  nsRefPtr<Connection> msc = new Connection(this);
+  // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility
+  // reasons.
+  int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE |
+              SQLITE_OPEN_CREATE;
+  nsRefPtr<Connection> msc = new Connection(this, flags);
   NS_ENSURE_TRUE(msc, NS_ERROR_OUT_OF_MEMORY);
 
-  int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE;
-  nsresult rv = msc->initialize(aDatabaseFile, flags);
+  nsresult rv = msc->initialize(aDatabaseFile);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NS_ADDREF(*_connection = msc);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 Service::OpenUnsharedDatabase(nsIFile *aDatabaseFile,
@@ -396,21 +399,24 @@ Service::OpenUnsharedDatabase(nsIFile *a
 {
 #ifdef NS_FUNCTION_TIMER
   nsCString leafname;
   (void)aDatabaseFile->GetNativeLeafName(leafname);
   NS_TIME_FUNCTION_FMT("mozIStorageService::OpenUnsharedDatabase(%s)",
                        leafname.get());
 #endif
 
-  nsRefPtr<Connection> msc = new Connection(this);
+  // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility
+  // reasons.
+  int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_PRIVATECACHE |
+              SQLITE_OPEN_CREATE;
+  nsRefPtr<Connection> msc = new Connection(this, flags);
   NS_ENSURE_TRUE(msc, NS_ERROR_OUT_OF_MEMORY);
 
-  int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_PRIVATECACHE;
-  nsresult rv = msc->initialize(aDatabaseFile, flags);
+  nsresult rv = msc->initialize(aDatabaseFile);
   NS_ENSURE_SUCCESS(rv, rv);
 
   NS_ADDREF(*_connection = msc);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 Service::BackupDatabaseFile(nsIFile *aDBFile,
--- a/storage/test/unit/test_storage_connection.js
+++ b/storage/test/unit/test_storage_connection.js
@@ -301,16 +301,192 @@ function test_close_fails_with_async_sta
     db.asyncClose(function() {
       // Reset gDBConn so that later tests will get a new connection object.
       gDBConn = null;
       run_next_test();
     });
   }
 }
 
+function test_clone_optional_param()
+{
+  let db1 = getService().openUnsharedDatabase(getTestDB());
+  let db2 = db1.clone();
+  do_check_true(db2.connectionReady);
+
+  // A write statement should not fail here.
+  let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+  stmt.params.name = "dwitte";
+  stmt.execute();
+  stmt.finalize();
+
+  // And a read statement should succeed.
+  stmt = db2.createStatement("SELECT * FROM test");
+  do_check_true(stmt.executeStep());
+  stmt.finalize();
+
+  // Additionally check that it is a connection on the same database.
+  do_check_true(db1.databaseFile.equals(db2.databaseFile));
+
+  run_next_test();
+}
+
+function test_clone_readonly()
+{
+  let db1 = getService().openUnsharedDatabase(getTestDB());
+  let db2 = db1.clone(true);
+  do_check_true(db2.connectionReady);
+
+  // A write statement should fail here.
+  let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+  stmt.params.name = "reed";
+  expectError(Cr.NS_ERROR_FILE_READ_ONLY, function() stmt.execute());
+  stmt.finalize();
+
+  // And a read statement should succeed.
+  stmt = db2.createStatement("SELECT * FROM test");
+  do_check_true(stmt.executeStep());
+  stmt.finalize();
+
+  run_next_test();
+}
+
+function test_clone_shared_readonly()
+{
+  let db1 = getService().openDatabase(getTestDB());
+  let db2 = db1.clone(true);
+  do_check_true(db2.connectionReady);
+
+  // A write statement should fail here.
+  let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)");
+  stmt.params.name = "reed";
+  // TODO currently SQLite does not actually work correctly here.  The behavior
+  //      we want is commented out, and the current behavior is being tested
+  //      for.  Our IDL comments will have to be updated when this starts to
+  //      work again.
+  stmt.execute(); // This should not throw!
+  //expectError(Cr.NS_ERROR_FILE_READ_ONLY, function() stmt.execute());
+  stmt.finalize();
+
+  // And a read statement should succeed.
+  stmt = db2.createStatement("SELECT * FROM test");
+  do_check_true(stmt.executeStep());
+  stmt.finalize();
+
+  run_next_test();
+}
+
+function test_close_clone_fails()
+{
+  let calls = [
+    "openDatabase",
+    "openUnsharedDatabase",
+  ];
+  calls.forEach(function(methodName) {
+    let db = getService()[methodName](getTestDB());
+    db.close();
+    expectError(Cr.NS_ERROR_NOT_INITIALIZED, function() db.clone());
+  });
+
+  run_next_test();
+}
+
+function test_memory_clone_fails()
+{
+  let db = getService().openSpecialDatabase("memory");
+  db.close();
+  expectError(Cr.NS_ERROR_NOT_INIALIZED, function() db.clone());
+
+  run_next_test();
+}
+
+function test_clone_copies_functions()
+{
+  const FUNC_NAME = "test_func";
+  let calls = [
+    "openDatabase",
+    "openUnsharedDatabase",
+  ];
+  let functionMethods = [
+    "createFunction",
+    "createAggregateFunction",
+  ];
+  calls.forEach(function(methodName) {
+    [true, false].forEach(function(readOnly) {
+      functionMethods.forEach(function(functionMethod) {
+        let db1 = getService()[methodName](getTestDB());
+        // Create a function for db1.
+        db1[functionMethod](FUNC_NAME, 1, {
+          onFunctionCall: function() 0,
+          onStep: function() 0,
+          onFinal: function() 0,
+        });
+
+        // Clone it, and make sure the function exists still.
+        let db2 = db1.clone(readOnly);
+        // Note: this would fail if the function did not exist.
+        let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test");
+        stmt.finalize();
+        db1.close();
+        db2.close();
+      });
+    });
+  });
+
+  run_next_test();
+}
+
+function test_clone_copies_overridden_functions()
+{
+  const FUNC_NAME = "lower";
+  function test_func() {
+    this.called = false;
+  }
+  test_func.prototype = {
+    onFunctionCall: function() {
+      this.called = true;
+    },
+    onStep: function() {
+      this.called = true;
+    },
+    onFinal: function() 0,
+  };
+
+  let calls = [
+    "openDatabase",
+    "openUnsharedDatabase",
+  ];
+  let functionMethods = [
+    "createFunction",
+    "createAggregateFunction",
+  ];
+  calls.forEach(function(methodName) {
+    [true, false].forEach(function(readOnly) {
+      functionMethods.forEach(function(functionMethod) {
+        let db1 = getService()[methodName](getTestDB());
+        // Create a function for db1.
+        let func = new test_func();
+        db1[functionMethod](FUNC_NAME, 1, func);
+        do_check_false(func.called);
+
+        // Clone it, and make sure the function gets called.
+        let db2 = db1.clone(readOnly);
+        let stmt = db2.createStatement("SELECT " + FUNC_NAME + "(id) FROM test");
+        stmt.executeStep();
+        do_check_true(func.called);
+        stmt.finalize();
+        db1.close();
+        db2.close();
+      });
+    });
+  });
+
+  run_next_test();
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Test Runner
 
 var tests = [
   test_connectionReady_open,
   test_connectionReady_closed,
   test_databaseFile,
   test_tableExists_not_created,
@@ -327,16 +503,21 @@ var tests = [
   test_set_schemaVersion,
   test_set_schemaVersion_same,
   test_set_schemaVersion_negative,
   test_createTable,
   test_defaultSynchronousAtNormal,
   test_close_does_not_spin_event_loop, // must be ran before executeAsync tests
   test_asyncClose_succeeds_with_finalized_async_statement,
   test_close_fails_with_async_statement_ran,
+  test_clone_optional_param,
+  test_clone_readonly,
+  test_close_clone_fails,
+  test_clone_copies_functions,
+  test_clone_copies_overridden_functions,
 ];
 let index = 0;
 
 function run_next_test()
 {
   function _run_next_test() {
     if (index < tests.length) {
       do_test_pending();