[geary/wip/726281-text-attachment-crlf: 2/13] Make database classes more amenable to asynchronous use.



commit 743b24dd5daa6c2082298c33016242904235e7d6
Author: Michael James Gratton <mike vee net>
Date:   Tue May 8 09:18:06 2018 +0930

    Make database classes more amenable to asynchronous use.
    
    This makes both the open() and open_connection() methods on
    Geary.DB.Database asynchronous, which allows the VersionedDatabase
    open_background() and its hackery to be removed, and upgrades to be
    performed asynchronously as well. It also adds a
    exec_transaction_async() method to Connection, allowing an existing
    object to also be used to establish an async transaction.
    
    * src/engine/db/db-connection.vala (Connection): Add
      exec_transaction_async method, update doc comments.
    
    * src/engine/db/db-database.vala (Database): Make open and
      open_connection async by executing SQLite code in a background thread,
      update call sites. Move job management code out of
      exec_transaction_async into a new internal add_async_job() method so it
      can be used by Connection. Add unit tests.
    
    * src/engine/db/db-transaction-async-job.vala (TransactionAsyncJob): Add
      an optional internal connection method and make the job's cancellable
      an internal property so a Connection instance can specify itself for
      the transaction.
    
    * src/engine/db/db-versioned-database.vala (VersionedDatabase): Remove
      open_background() hack since open() is now async. Make version upgrade
      hooks for derived classes async and update call sites. Use a
      Nonblocking.Mutex rather than GLib mutex so upgrade exclusion works
      asynchronously. Add unit tests.
    
    * src/engine/imap-db/imap-db-database.vala (Database): Make database
      upgrade methods async and execute SQL in async instructions now that
      the bases classes support it. Add unit tests.

 src/engine/db/db-connection.vala               |   65 +++-
 src/engine/db/db-database.vala                 |  154 ++++++----
 src/engine/db/db-transaction-async-job.vala    |   36 ++-
 src/engine/db/db-versioned-database.vala       |  283 +++++++++-------
 src/engine/imap-db/imap-db-account.vala        |    2 +-
 src/engine/imap-db/imap-db-database.vala       |  416 +++++++++++-------------
 src/engine/imap-db/imap-db-gc.vala             |    7 +-
 test/CMakeLists.txt                            |    3 +
 test/engine/db/db-database-test.vala           |  125 +++++++
 test/engine/db/db-versioned-database-test.vala |   54 +++
 test/engine/imap-db/imap-db-database-test.vala |   48 +++
 test/meson.build                               |    3 +
 test/test-engine.vala                          |    3 +
 13 files changed, 762 insertions(+), 437 deletions(-)
---
diff --git a/src/engine/db/db-connection.vala b/src/engine/db/db-connection.vala
index c9390cb..ff44d4c 100644
--- a/src/engine/db/db-connection.vala
+++ b/src/engine/db/db-connection.vala
@@ -1,16 +1,19 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
 /**
- * A Connection represents a connection to an open database.  Because SQLite uses a 
- * synchronous interface, all calls are blocking.  Db.Database offers asynchronous queries by
- * pooling connections and invoking queries from background threads.
+ * A Connection represents a connection to an open database.
  *
- * Connections are associated with a Database.  Use Database.open_connection() to create
- * one.
+ * Because SQLite uses a synchronous interface, all calls are
+ * blocking.  Db.Database offers asynchronous queries by pooling
+ * connections and invoking queries from background threads.
+ *
+ * Connections are associated with a Database.  Use
+ * Database.open_connection() to create one.
  *
  * A Connection will close when its last reference is dropped.
  */
@@ -97,8 +100,8 @@ public class Geary.Db.Connection : Geary.Db.Context {
      *
      * There is no way to retrieve a result iterator from this call.
      *
-     * This may be called from a TransactionMethod called within exec_transaction() or
-     * Db.Database.exec_transaction_async().
+     * This may be called from a TransactionMethod called within
+     * {@link exec_transaction} or {@link exec_transaction_async}.
      *
      * See [[http://www.sqlite.org/c3ref/exec.html]]
      */
@@ -116,8 +119,8 @@ public class Geary.Db.Connection : Geary.Db.Context {
      *
      * There is no way to retrieve a result iterator from this call.
      *
-     * This can be called from a TransactionMethod called within exec_transaction() or
-     * Db.Database.exec_transaction_async().
+     * This may be called from a TransactionMethod called within
+     * {@link exec_transaction} or {@link exec_transaction_async}.
      */
     public void exec_file(File file, Cancellable? cancellable = null) throws Error {
         check_cancelled("Connection.exec_file", cancellable);
@@ -150,14 +153,18 @@ public class Geary.Db.Connection : Geary.Db.Context {
     public int get_busy_timeout_msec() {
         return busy_timeout_msec;
     }
-    
+
     /**
-     * Sets busy timeout time in milliseconds.  Zero or a negative value indicates that all
-     * operations that SQLite returns BUSY will be retried until they complete with success or error.
-     * Otherwise, after said amount of time has transpired, DatabaseError.BUSY will be thrown.
+     * Sets busy timeout time in milliseconds.
      *
-     * This is imperative for exec_transaction() and Db.Database.exec_transaction_async(), because
-     * those calls will throw a DatabaseError.BUSY call immediately if another transaction has
+     * Zero or a negative value indicates that all operations that
+     * SQLite returns BUSY will be retried until they complete with
+     * success or error.  Otherwise, after said amount of time has
+     * transpired, DatabaseError.BUSY will be thrown.
+     *
+     * This is imperative for {@link exec_transaction} {@link
+     * exec_transaction_async}, because those calls will throw a
+     * DatabaseError.BUSY call immediately if another transaction has
      * acquired the reserved or exclusive locks.
      */
     public void set_busy_timeout_msec(int busy_timeout_msec) throws Error {
@@ -415,7 +422,31 @@ public class Geary.Db.Connection : Geary.Db.Context {
         
         return outcome;
     }
-    
+
+    /**
+     * Starts a new asynchronous transaction for this connection.
+     *
+     * Asynchronous transactions are handled via background
+     * threads. The background thread calls {@link exec_transaction};
+     * see that method for more information about coding a
+     * transaction. The only caveat is that the {@link
+     * TransactionMethod} passed to it must be thread-safe.
+     *
+     * Throws {@link DatabaseError.OPEN_REQUIRED} if not open.
+     */
+    public async TransactionOutcome exec_transaction_async(TransactionType type,
+                                                           TransactionMethod cb,
+                                                           Cancellable? cancellable)
+        throws Error {
+        // create job to execute in background thread
+        TransactionAsyncJob job = new TransactionAsyncJob(
+            this, type, cb, cancellable
+        );
+
+        this.database.add_async_job(job);
+        return yield job.wait_for_completion_async();
+    }
+
     public override Connection? get_connection() {
         return this;
     }
diff --git a/src/engine/db/db-database.vala b/src/engine/db/db-database.vala
index 7312032..a558ef1 100644
--- a/src/engine/db/db-database.vala
+++ b/src/engine/db/db-database.vala
@@ -1,19 +1,23 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2018 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
 /**
- * Database represents an SQLite file.  Multiple Connections may be opened to against the
- * Database.
+ * Represents a single SQLite database.
  *
- * Since it's often just more bookkeeping to maintain a single global connection, Database also
- * offers a master connection which may be used to perform queries and transactions.
+ * Each database supports multiple {@link Connection}s that allow SQL
+ * queries to be executed, however if a single connection is required
+ * by an app, this class also provides convenience methods to execute
+ * queries against a common ''master'' connection.
  *
- * Database also offers asynchronous transactions which work via connection and thread pools.
- *
- * NOTE: In-memory databases are currently unsupported.
+ * This class offers a number of asynchronous methods, however since
+ * SQLite only supports a synchronous API, these are implemented using
+ * a pool of background threads. Asynchronous transactions are
+ * available via {@link exec_transaction_async}.
  */
 
 public class Geary.Db.Database : Geary.Db.Context {
@@ -85,27 +89,42 @@ public class Geary.Db.Database : Geary.Db.Context {
             assert(outstanding_async_jobs == 0);
         }
     }
-    
+
     /**
-     * Opens the Database, creating any files and directories it may need in the process depending
-     * on the DatabaseFlags.
+     * Prepares the database for use.
      *
-     * NOTE: A Database may be closed, but the Connections it creates will always be valid as
-     * they hold a reference to their source Database.  To release a Database's resources, drop all
-     * references to it and its associated Connections, Statements, and Results.
+     * This will create any needed files and directories, check the
+     * database's integrity, and so on, depending on the flags passed
+     * to this method.
+     *
+     * NOTE: A Database may be closed, but the Connections it creates
+     * will always be valid as they hold a reference to their source
+     * Database. To release a Database's resources, drop all
+     * references to it and its associated Connections, Statements,
+     * and Results.
      */
-    public virtual void open(DatabaseFlags flags, PrepareConnection? prepare_cb,
-        Cancellable? cancellable = null) throws Error {
+    public virtual async void open(DatabaseFlags flags,
+                                   PrepareConnection? prepare_cb,
+                                   Cancellable? cancellable = null)
+        throws Error {
         if (is_open)
             return;
-        
+
         this.flags = flags;
         this.prepare_cb = prepare_cb;
 
         if (this.file != null && (flags & DatabaseFlags.CREATE_DIRECTORY) != 0) {
-            File db_dir = this.file.get_parent();
-            if (!db_dir.query_exists(cancellable))
+            GLib.File db_dir = this.file.get_parent();
+            try {
+                yield db_dir.query_info_async(
+                    GLib.FileAttribute.STANDARD_TYPE,
+                    GLib.FileQueryInfoFlags.NONE,
+                    GLib.Priority.DEFAULT,
+                    cancellable
+                );
+            } catch (GLib.IOError.NOT_FOUND err) {
                 db_dir.make_directory_with_parents(cancellable);
+            }
         }
 
         if (threadsafe()) {
@@ -116,13 +135,16 @@ public class Geary.Db.Database : Geary.Db.Context {
         } else {
             warning("SQLite not thread-safe: asynchronous queries will not be available");
         }
-        
-        if ((flags & DatabaseFlags.CHECK_CORRUPTION) != 0)
-            check_for_corruption(flags, cancellable);
-        
+
+        if ((flags & DatabaseFlags.CHECK_CORRUPTION) != 0) {
+            yield Nonblocking.Concurrent.global.schedule_async(() => {
+                    check_for_corruption(flags, cancellable);
+                }, cancellable);
+        }
+
         is_open = true;
     }
-    
+
     private void check_for_corruption(DatabaseFlags flags, Cancellable? cancellable) throws Error {
         // Open a connection and test for corruption by creating a dummy table,
         // adding a row, selecting the row, then dropping the table ... can only do this for
@@ -193,10 +215,15 @@ public class Geary.Db.Database : Geary.Db.Context {
     /**
      * Throws DatabaseError.OPEN_REQUIRED if not open.
      */
-    public Connection open_connection(Cancellable? cancellable = null) throws Error {
-        return internal_open_connection(false, cancellable);
+    public async Connection open_connection(Cancellable? cancellable = null)
+        throws Error {
+        Connection? cx = null;
+        yield Nonblocking.Concurrent.global.schedule_async(() => {
+                cx = internal_open_connection(false, cancellable);
+            }, cancellable);
+        return cx;
     }
-    
+
     private Connection internal_open_connection(bool master, Cancellable? cancellable) throws Error {
         check_open();
 
@@ -277,45 +304,60 @@ public class Geary.Db.Database : Geary.Db.Context {
         Cancellable? cancellable = null) throws Error {
         return get_master_connection().exec_transaction(type, cb, cancellable);
     }
-    
+
     /**
-     * Asynchronous transactions are handled via background threads using a pool of Connections.
-     * The background thread calls Connection.exec_transaction(); see that method for more
-     * information about coding a transaction.  The only caveat is that the TransactionMethod
-     * must be thread-safe.
+     * Starts a new asynchronous transaction using a new connection.
      *
-     * Throws DatabaseError.OPEN_REQUIRED if not open.
+     * Asynchronous transactions are handled via background
+     * threads. The background thread opens a new connection, and
+     * calls {@link Connection.exec_transaction}; see that method for
+     * more information about coding a transaction. The only caveat is
+     * that the {@link TransactionMethod} passed to it must be
+     * thread-safe.
+     *
+     * Throws {@link DatabaseError.OPEN_REQUIRED} if not open.
      */
-    public async TransactionOutcome exec_transaction_async(TransactionType type, TransactionMethod cb,
-        Cancellable? cancellable) throws Error {
+    public async TransactionOutcome exec_transaction_async(TransactionType type,
+                                                           TransactionMethod cb,
+                                                           Cancellable? cancellable)
+        throws Error {
+        TransactionAsyncJob job = new TransactionAsyncJob(
+            null, type, cb, cancellable
+        );
+        add_async_job(job);
+        return yield job.wait_for_completion_async();
+    }
+
+    /** Adds the given job to the thread pool. */
+    internal void add_async_job(TransactionAsyncJob new_job) throws Error {
         check_open();
-        
-        if (thread_pool == null)
-            throw new DatabaseError.GENERAL("SQLite thread safety disabled, async operations unallowed");
-        
-        // create job to execute in background thread
-        TransactionAsyncJob job = new TransactionAsyncJob(type, cb, cancellable);
-        
-        lock (outstanding_async_jobs) {
-            outstanding_async_jobs++;
+
+        if (this.thread_pool == null) {
+            throw new DatabaseError.GENERAL(
+                "SQLite thread safety disabled, async operations unallowed"
+            );
         }
-        
-        thread_pool.add(job);
-        
-        return yield job.wait_for_completion_async();
+
+        lock (this.outstanding_async_jobs) {
+            this.outstanding_async_jobs++;
+        }
+
+        this.thread_pool.add(new_job);
     }
-    
+
     // This method must be thread-safe.
     private void on_async_job(owned TransactionAsyncJob job) {
         // *never* use master connection for threaded operations
-        Connection? cx = null;
+        Connection? cx = job.cx;
         Error? open_err = null;
-        try {
-            cx = open_connection();
-        } catch (Error err) {
-            open_err = err;
-            debug("Warning: unable to open database connection to %s, cancelling AsyncJob: %s",
-                  this.path, err.message);
+        if (cx == null) {
+            try {
+                cx = internal_open_connection(false, job.cancellable);
+            } catch (Error err) {
+                open_err = err;
+                debug("Warning: unable to open database connection to %s, cancelling AsyncJob: %s",
+                      this.path, err.message);
+            }
         }
 
         if (cx != null)
diff --git a/src/engine/db/db-transaction-async-job.vala b/src/engine/db/db-transaction-async-job.vala
index 3f278f3..9e41968 100644
--- a/src/engine/db/db-transaction-async-job.vala
+++ b/src/engine/db/db-transaction-async-job.vala
@@ -1,25 +1,34 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
 private class Geary.Db.TransactionAsyncJob : BaseObject {
+
+    internal Connection? cx { get; private set; default = null; }
+    internal Cancellable cancellable { get; private set; }
+
     private TransactionType type;
     private unowned TransactionMethod cb;
-    private Cancellable cancellable;
     private Nonblocking.Event completed;
     private TransactionOutcome outcome = TransactionOutcome.ROLLBACK;
     private Error? caught_err = null;
-    
-    public TransactionAsyncJob(TransactionType type, TransactionMethod cb, Cancellable? cancellable) {
+
+
+    public TransactionAsyncJob(Connection? cx,
+                               TransactionType type,
+                               TransactionMethod cb,
+                               Cancellable? cancellable) {
+        this.cx = cx;
         this.type = type;
         this.cb = cb;
         this.cancellable = cancellable ?? new Cancellable();
-        
-        completed = new Nonblocking.Event();
+
+        this.completed = new Nonblocking.Event();
     }
-    
+
     public void cancel() {
         cancellable.cancel();
     }
@@ -82,17 +91,16 @@ private class Geary.Db.TransactionAsyncJob : BaseObject {
         
         return false;
     }
-    
+
     // No way to cancel this because the callback thread *must* finish before
     // we move on here.  Any I/O the thread is doing can still be cancelled
     // using our cancel() above.
     public async TransactionOutcome wait_for_completion_async()
         throws Error {
-        yield completed.wait_async();
-        if (caught_err != null)
-            throw caught_err;
-        
-        return outcome;
+        yield this.completed.wait_async();
+        if (this.caught_err != null)
+            throw this.caught_err;
+
+        return this.outcome;
     }
 }
-
diff --git a/src/engine/db/db-versioned-database.vala b/src/engine/db/db-versioned-database.vala
index e4a8b57..8992e79 100644
--- a/src/engine/db/db-versioned-database.vala
+++ b/src/engine/db/db-versioned-database.vala
@@ -1,13 +1,32 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2018 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
+/**
+ * A SQLite database with a versioned, upgradeable schema.
+ *
+ * This class uses the SQLite user version pragma to track the current
+ * version of a database, and a set of SQL scripts (one per version)
+ * to manage updating from one version to another. When the database
+ * is first opened by a call to {@link open}, its current version is
+ * checked against the set of available scripts, and each available
+ * version script above the current version is applied in
+ * order. Derived classes may override the {@link pre_upgrade} and
+ * {@link post_upgrade} methods to perform additional work before and
+ * after an upgrade script is executed, and {@link starting_upgrade}
+ * and {@link completed_upgrade} to be notified of the upgrade process
+ * starting and finishing.
+ */
 public class Geary.Db.VersionedDatabase : Geary.Db.Database {
-    public delegate void WorkCallback();
 
-    private static Mutex upgrade_mutex = Mutex();
+
+    private static Geary.Nonblocking.Mutex upgrade_mutex =
+        new Geary.Nonblocking.Mutex();
+
 
     public File schema_dir { get; private set; }
 
@@ -26,58 +45,48 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
     /**
      * Called by {@link open} if a schema upgrade is required and beginning.
      *
-     * If called by {@link open_background}, this will be called in the context of a background
-     * thread.
-     *
      * If new_db is set to true, the database is being created from scratch.
      */
     protected virtual void starting_upgrade(int current_version, bool new_db) {
     }
-    
+
     /**
      * Called by {@link open} just before performing a schema upgrade step.
-     *
-     * If called by {@link open_background}, this will be called in the context of a background
-     * thread.
      */
-    protected virtual void pre_upgrade(int version) {
+    protected virtual async void pre_upgrade(int version, Cancellable? cancellable)
+        throws Error {
     }
-    
+
     /**
      * Called by {@link open} just after performing a schema upgrade step.
-     *
-     * If called by {@link open_background}, this will be called in the context of a background
-     * thread.
      */
-    protected virtual void post_upgrade(int version) {
+    protected virtual async void post_upgrade(int version, Cancellable? cancellable)
+        throws Error {
     }
-    
+
     /**
      * Called by {@link open} if a schema upgrade was required and has now completed.
-     *
-     * If called by {@link open_background}, this will be called in the context of a background
-     * thread.
      */
     protected virtual void completed_upgrade(int final_version) {
     }
-    
-    private File get_schema_file(int db_version) {
-        return schema_dir.get_child("version-%03d.sql".printf(db_version));
-    }
-    
+
     /**
-     * Creates or opens the database, initializing and upgrading the schema.
+     * Prepares the database for use, initializing and upgrading the schema.
      *
-     * If it's detected that the database has a schema version that's unavailable in the schema
-     * directory, throws {@link DatabaseError.SCHEMA_VERSION}.  Generally this indicates the
-     * user attempted to load the database with an older version of the application.
+     * If it's detected that the database has a schema version that's
+     * unavailable in the schema directory, throws {@link
+     * DatabaseError.SCHEMA_VERSION}.  Generally this indicates the
+     * user attempted to load the database with an older version of
+     * the application.
      */
-    public override void open(DatabaseFlags flags, PrepareConnection? prepare_cb,
-        Cancellable? cancellable = null) throws Error {
-        base.open(flags, prepare_cb, cancellable);
-        
+    public override async void open(DatabaseFlags flags,
+                                    PrepareConnection? prepare_cb,
+                                    Cancellable? cancellable = null)
+        throws Error {
+        yield base.open(flags, prepare_cb, cancellable);
+
         // get Connection for upgrade activity
-        Connection cx = open_connection(cancellable);
+        Connection cx = yield open_connection(cancellable);
         
         int db_version = cx.get_user_version_number();
         debug("VersionedDatabase.upgrade: current database schema for %s: %d",
@@ -90,117 +99,137 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
         // Initialize new database to version 1 (note the preincrement in the loop below)
         if (db_version < 0)
             db_version = 0;
-        
-        // Check for database schemas newer than what's available in the schema directory; this
-        // happens some times in development or if a user attempts to roll back their version
-        // of the app without restoring a backup of the database ... since schema is so important
-        // to database coherency, need to protect against both
-        //
-        // Note that this is checking for a schema file for the current version of the database
-        // (assuming it's version 1 or better); the next check autoincrements to look for the
-        // *next* version of the database
-        if (db_version > 0 && !get_schema_file(db_version).query_exists(cancellable)) {
-            throw new DatabaseError.SCHEMA_VERSION("%s schema %d unknown to current schema plan",
-                this.path, db_version);
+
+        if (db_version > 0) {
+            // Check for database schemas newer than what's available
+            // in the schema directory; this happens some times in
+            // development or if a user attempts to roll back their
+            // version of the app without restoring a backup of the
+            // database ... since schema is so important to database
+            // coherency, need to protect against both
+            //
+            // Note that this is checking for a schema file for the
+            // current version of the database (assuming it's version
+            // 1 or better); the next check autoincrements to look for
+            // the *next* version of the database
+            if (!yield exists(get_schema_file(db_version), cancellable)) {
+                throw new DatabaseError.SCHEMA_VERSION(
+                    "%s schema %d unknown to current schema plan",
+                    this.path, db_version
+                );
+            }
         }
 
         // Go through all the version scripts in the schema directory and apply each of them.
         bool started = false;
         for (;;) {
             File upgrade_script = get_schema_file(++db_version);
-            if (!upgrade_script.query_exists(cancellable))
+            if (!yield exists(upgrade_script, cancellable)) {
                 break;
-            
+            }
+
             if (!started) {
                 starting_upgrade(db_version, new_db);
                 started = true;
             }
-            
-            // Since these upgrades run in a background thread, there's a possibility they
-            // can run in parallel.  That leads to Geary absolutely taking over the machine,
-            // with potentially several threads all doing heavy database manipulation at
-            // once.  So, we wrap this bit in a mutex lock so that only one database is
-            // updating at once.  It means overall it might take a bit longer, but it keeps
-            // things usable in the meantime.  See <https://bugzilla.gnome.org/show_bug.cgi?id=724475>.
-            upgrade_mutex.@lock();
-            
-            pre_upgrade(db_version);
-            
-            check_cancelled("VersionedDatabase.open", cancellable);
-            
+
+            // Since these upgrades run in a background thread,
+            // there's a possibility they can run in parallel.  That
+            // leads to Geary absolutely taking over the machine, with
+            // potentially several threads all doing heavy database
+            // manipulation at once.  So, we wrap this bit in a mutex
+            // lock so that only one database is updating at once.  It
+            // means overall it might take a bit longer, but it keeps
+            // things usable in the meantime.  See
+            // <https://bugzilla.gnome.org/show_bug.cgi?id=724475>.
+            int token = yield VersionedDatabase.upgrade_mutex.claim_async(
+                cancellable
+            );
+
+            Error? locked_err = null;
             try {
-                debug("Upgrading database to version %d with %s", db_version, upgrade_script.get_path());
-                cx.exec_transaction(TransactionType.EXCLUSIVE, (cx) => {
-                    cx.exec_file(upgrade_script, cancellable);
-                    cx.set_user_version_number(db_version);
-                    
-                    return TransactionOutcome.COMMIT;
-                }, cancellable);
+                yield execute_upgrade(
+                    cx, db_version, upgrade_script, cancellable
+                );
             } catch (Error err) {
-                warning("Error upgrading database to version %d: %s", db_version, err.message);
-                upgrade_mutex.unlock();
-                
-                throw err;
+                locked_err = err;
+            }
+
+            VersionedDatabase.upgrade_mutex.release(ref token);
+
+            if (locked_err != null) {
+                throw locked_err;
             }
-            
-            post_upgrade(db_version);
-            
-            upgrade_mutex.unlock();
         }
-        
+
         if (started)
             completed_upgrade(db_version);
     }
-    
-    /**
-     * Opens the database in a background thread so foreground work can be performed while updating.
-     *
-     * Since {@link open} may take a considerable amount of time for a {@link VersionedDatabase},
-     * background_open() can be used to perform that work in a thread while the calling thread
-     * "pumps" a {@link WorkCallback} every work_cb_msec milliseconds.  In general, this is
-     * designed for allowing an event queue to execute tasks or update a progress monitor of some
-     * kind.
-     *
-     * Note that the database is not opened while the callback is executing and so it should not
-     * call into the database (unless it's a call safe to use prior to open).
-     *
-     * If work_cb_sec is zero or less, WorkCallback is called continuously, which may or may not be
-     * desired.
-     *
-     * @see open
-     */
-    public void open_background(DatabaseFlags flags, PrepareConnection? prepare_cb,
-        WorkCallback work_cb, int work_cb_msec, Cancellable? cancellable = null) throws Error {
-        // use a SpinWaiter to safely wait for the thread to exit while occassionally calling the
-        // WorkCallback (which can not abort in current impl.) to do foreground work.
-        Synchronization.SpinWaiter waiter = new Synchronization.SpinWaiter(work_cb_msec, () => {
-            work_cb();
-            
-            // continue (never abort)
-            return true;
-        });
-        
-        // do the open in a background thread
-        Error? thread_err = null;
-        Thread<bool> thread = new Thread<bool>.try("Geary.Db.VersionedDatabase.open()", () => {
-            try {
-                open(flags, prepare_cb, cancellable);
-            } catch (Error err) {
-                thread_err = err;
+
+    private async void execute_upgrade(Connection cx,
+                                       int db_version,
+                                       GLib.File upgrade_script,
+                                       Cancellable? cancellable)
+        throws Error {
+        debug("Upgrading database to version %d with %s",
+              db_version, upgrade_script.get_path());
+
+        check_cancelled("VersionedDatabase.open", cancellable);
+        try {
+            yield pre_upgrade(db_version, cancellable);
+        } catch (Error err) {
+            if (!(err is IOError.CANCELLED)) {
+                warning("Error executing pre-upgrade for version %d: %s",
+                        db_version, err.message);
             }
-            
-            // notify the foreground waiter we're done
-            waiter.notify();
-            
-            return true;
-        });
-        
-        // wait until thread is completed and then dispose of it
-        waiter.wait();
-        thread = null;
-        
-        if (thread_err != null)
-            throw thread_err;
+            throw err;
+        }
+
+        check_cancelled("VersionedDatabase.open", cancellable);
+        try {
+            yield cx.exec_transaction_async(TransactionType.EXCLUSIVE, (cx) => {
+                    cx.exec_file(upgrade_script, cancellable);
+                    cx.set_user_version_number(db_version);
+
+                    return TransactionOutcome.COMMIT;
+                }, cancellable);
+        } catch (Error err) {
+            if (!(err is IOError.CANCELLED)) {
+                warning("Error upgrading database to version %d: %s",
+                        db_version, err.message);
+            }
+            throw err;
+        }
+
+        check_cancelled("VersionedDatabase.open", cancellable);
+        try {
+            yield post_upgrade(db_version, cancellable);
+        } catch (Error err) {
+            if (!(err is IOError.CANCELLED)) {
+                warning("Error executing post-upgrade for version %d: %s",
+                        db_version, err.message);
+            }
+            throw err;
+        }
     }
-}
 
+    private File get_schema_file(int db_version) {
+        return schema_dir.get_child("version-%03d.sql".printf(db_version));
+    }
+
+    private async bool exists(GLib.File target, Cancellable? cancellable) {
+        bool ret = true;
+        try {
+            yield target.query_info_async(
+                GLib.FileAttribute.STANDARD_TYPE,
+                    GLib.FileQueryInfoFlags.NONE,
+                    GLib.Priority.DEFAULT,
+                    cancellable
+                );
+        } catch (Error err) {
+            ret = false;
+        }
+        return ret;
+    }
+
+}
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index cfa9f8e..a91acd0 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -264,7 +264,7 @@ private class Geary.ImapDB.Account : BaseObject {
             account_information.primary_mailbox.address);
         
         try {
-            yield db.open_async(
+            yield db.open(
                 Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE | 
Db.DatabaseFlags.CHECK_CORRUPTION,
                 cancellable);
         } catch (Error err) {
diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala
index 61c7510..d7686d2 100644
--- a/src/engine/imap-db/imap-db-database.vala
+++ b/src/engine/imap-db/imap-db-database.vala
@@ -1,4 +1,5 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later).  See the COPYING file in this distribution.
@@ -34,15 +35,12 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
     }
     
     /**
-     * Opens the ImapDB database.
-     *
-     * This should only be done from the main thread, as it is designed to pump the event loop
-     * while the database is being opened and updated.
+     * Prepares the ImapDB database for use.
      */
-    public async void open_async(Db.DatabaseFlags flags, Cancellable? cancellable) throws Error {
-        open_background(flags, on_prepare_database_connection, pump_event_loop,
-            OPEN_PUMP_EVENT_LOOP_MSEC, cancellable);
-        
+    public new async void open(Db.DatabaseFlags flags, Cancellable? cancellable)
+        throws Error {
+        yield base.open(flags, on_prepare_database_connection, cancellable);
+
         // Tie user-supplied Cancellable to internal Cancellable, which is used when close() is
         // called
         if (cancellable != null)
@@ -103,140 +101,133 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
         
         base.close(cancellable);
     }
-    
-    private void pump_event_loop() {
-        while (MainContext.default().pending())
-            MainContext.default().iteration(true);
-    }
-    
+
     protected override void starting_upgrade(int current_version, bool new_db) {
         this.new_db = new_db;
-        // can't call the ProgressMonitor directly, as it's hooked up to signals that expect to be
-        // called in the foreground thread, so use the Idle loop for this
-        Idle.add(() => {
-            // don't use upgrade_monitor for new databases, as the upgrade should be near-
-            // instantaneous.  Also, there's some issue with GTK when starting the progress
-            // monitor while GtkDialog's are in play:
-            // https://bugzilla.gnome.org/show_bug.cgi?id=726269
-            if (!new_db && !upgrade_monitor.is_in_progress)
-                upgrade_monitor.notify_start();
-            
-            return false;
-        });
+
+        // don't use upgrade_monitor for new databases, as the upgrade should be near-
+        // instantaneous.  Also, there's some issue with GTK when starting the progress
+        // monitor while GtkDialog's are in play:
+        // https://bugzilla.gnome.org/show_bug.cgi?id=726269
+        if (!new_db && !upgrade_monitor.is_in_progress) {
+            upgrade_monitor.notify_start();
+        }
     }
-    
+
     protected override void completed_upgrade(int final_version) {
-        // see starting_upgrade() for explanation why this is done in Idle loop
-        Idle.add(() => {
-            if (!new_db && upgrade_monitor.is_in_progress)
-                upgrade_monitor.notify_finish();
-            
-            return false;
-        });
+        if (!new_db && upgrade_monitor.is_in_progress) {
+            upgrade_monitor.notify_finish();
+        }
     }
-    
-    protected override void post_upgrade(int version) {
+
+    protected async override void post_upgrade(int version,
+                                               Cancellable? cancellable)
+        throws Error {
         switch (version) {
             case 5:
-                post_upgrade_populate_autocomplete();
+                yield post_upgrade_populate_autocomplete(cancellable);
             break;
-            
+
             case 6:
-                post_upgrade_encode_folder_names();
+                yield post_upgrade_encode_folder_names(cancellable);
             break;
-            
+
             case 11:
-                post_upgrade_add_search_table();
+                yield post_upgrade_add_search_table(cancellable);
             break;
-            
+
             case 12:
-                post_upgrade_populate_internal_date_time_t();
+                yield post_upgrade_populate_internal_date_time_t(cancellable);
             break;
-            
+
             case 13:
-                post_upgrade_populate_additional_attachments();
+                yield post_upgrade_populate_additional_attachments(cancellable);
             break;
-            
+
             case 14:
-                post_upgrade_expand_page_size();
+                yield post_upgrade_expand_page_size(cancellable);
             break;
-            
+
             case 15:
-                post_upgrade_fix_localized_internaldates();
+                yield post_upgrade_fix_localized_internaldates(cancellable);
             break;
-            
+
             case 18:
-                post_upgrade_populate_internal_date_time_t();
+                yield post_upgrade_populate_internal_date_time_t(cancellable);
             break;
-            
+
             case 19:
-                post_upgrade_validate_contacts();
+                yield post_upgrade_validate_contacts(cancellable);
             break;
-            
+
             case 22:
-                post_upgrade_rebuild_attachments();
+                yield post_upgrade_rebuild_attachments(cancellable);
             break;
-            
+
             case 23:
-                post_upgrade_add_tokenizer_table();
+                yield post_upgrade_add_tokenizer_table(cancellable);
             break;
         }
     }
-    
+
     // Version 5.
-    private void post_upgrade_populate_autocomplete() {
-        try {
-            Db.Result result = query("SELECT sender, from_field, to_field, cc, bcc FROM MessageTable");
-            while (!result.finished) {
-                MessageAddresses message_addresses =
+    private async void post_upgrade_populate_autocomplete(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
+                Db.Result result = cx.query(
+                    "SELECT sender, from_field, to_field, cc, bcc FROM MessageTable"
+                );
+                while (!result.finished && !cancellable.is_cancelled()) {
+                    MessageAddresses message_addresses =
                     new MessageAddresses.from_result(account_owner_email, result);
-                foreach (Contact contact in message_addresses.contacts) {
-                    do_update_contact(get_master_connection(), contact, null);
+                    foreach (Contact contact in message_addresses.contacts) {
+                        do_update_contact(cx, contact, null);
+                    }
+                    result.next();
                 }
-                
-                result.next();
-            }
-        } catch (Error err) {
-            debug("Error populating autocompletion table during upgrade to database schema 5");
-        }
+                return Geary.Db.TransactionOutcome.COMMIT;
+            }, cancellable);
     }
-    
+
     // Version 6.
-    private void post_upgrade_encode_folder_names() {
-        try {
-            Db.Result select = query("SELECT id, name FROM FolderTable");
-            while (!select.finished) {
-                int64 id = select.int64_at(0);
-                string encoded_name = select.nonnull_string_at(1);
-                
-                try {
-                    string canonical_name = Geary.ImapUtf7.imap_utf7_to_utf8(encoded_name);
-                    
-                    Db.Statement update = prepare("UPDATE FolderTable SET name=? WHERE id=?");
-                    update.bind_string(0, canonical_name);
-                    update.bind_int64(1, id);
-                    update.exec();
-                } catch (Error e) {
-                    debug("Error renaming folder %s to its canonical representation: %s", encoded_name, 
e.message);
+    private async void post_upgrade_encode_folder_names(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
+                Db.Result select = cx.query("SELECT id, name FROM FolderTable");
+                while (!select.finished && !cancellable.is_cancelled()) {
+                    int64 id = select.int64_at(0);
+                    string encoded_name = select.nonnull_string_at(1);
+
+                    try {
+                        string canonical_name = Geary.ImapUtf7.imap_utf7_to_utf8(encoded_name);
+
+                        Db.Statement update = cx.prepare(
+                            "UPDATE FolderTable SET name=? WHERE id=?"
+                        );
+                        update.bind_string(0, canonical_name);
+                        update.bind_int64(1, id);
+                        update.exec();
+                    } catch (Error e) {
+                        debug("Error renaming folder %s to its canonical representation: %s", encoded_name, 
e.message);
+                    }
+
+                    select.next();
                 }
-                
-                select.next();
-            }
-        } catch (Error e) {
-            debug("Error decoding folder names during upgrade to database schema 6: %s", e.message);
-        }
+                return Geary.Db.TransactionOutcome.COMMIT;
+            }, cancellable);
     }
-    
+
     // Version 11.
-    private void post_upgrade_add_search_table() {
-        try {
-            string stemmer = find_appropriate_search_stemmer();
-            debug("Creating search table using %s stemmer", stemmer);
-            
-            // This can't go in the .sql file because its schema (the stemmer
-            // algorithm) is determined at runtime.
-            exec("""
-                CREATE VIRTUAL TABLE MessageSearchTable USING fts4(
+    private async void post_upgrade_add_search_table(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
+                string stemmer = find_appropriate_search_stemmer();
+                debug("Creating search table using %s stemmer", stemmer);
+
+                // This can't go in the .sql file because its schema (the stemmer
+                // algorithm) is determined at runtime.
+                cx.exec("""
+                    CREATE VIRTUAL TABLE MessageSearchTable USING fts4(
                     body,
                     attachment,
                     subject,
@@ -244,16 +235,15 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                     receivers,
                     cc,
                     bcc,
-                    
+
                     tokenize=unicodesn "stemmer=%s",
                     prefix="2,4,6,8,10",
                 );
-            """.printf(stemmer));
-        } catch (Error e) {
-            error("Error creating search table: %s", e.message);
-        }
+                """.printf(stemmer));
+                return Geary.Db.TransactionOutcome.COMMIT;
+            }, cancellable);
     }
-    
+
     private string find_appropriate_search_stemmer() {
         // Unfortunately, the stemmer library only accepts the full language
         // name for the stemming algorithm.  This translates between the user's
@@ -280,27 +270,30 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                 case "tr": return "turkish";
             }
         }
-        
+
         // Default to English because it seems to be on average the language
         // most likely to be present in emails, regardless of the user's
         // language setting.  This is not an exact science, and search results
         // should be ok either way in most cases.
         return "english";
     }
-    
+
     // Versions 12 and 18.
-    private void post_upgrade_populate_internal_date_time_t() {
-        try {
-            exec_transaction(Db.TransactionType.RW, (cx) => {
-                Db.Result select = cx.query("SELECT id, internaldate FROM MessageTable");
+    private async void
+        post_upgrade_populate_internal_date_time_t(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
+                Db.Result select = cx.query(
+                    "SELECT id, internaldate FROM MessageTable"
+                );
                 while (!select.finished) {
                     int64 id = select.rowid_at(0);
                     string? internaldate = select.string_at(1);
-                    
+
                     try {
                         time_t as_time_t = (internaldate != null ?
                             Geary.Imap.InternalDate.decode(internaldate).to_time_t() : -1);
-                        
+
                         Db.Statement update = cx.prepare(
                             "UPDATE MessageTable SET internaldate_time_t=? WHERE id=?");
                         update.bind_int64(0, (int64) as_time_t);
@@ -310,22 +303,19 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                         debug("Error converting internaldate '%s' to time_t: %s",
                             internaldate, e.message);
                     }
-                    
+
                     select.next();
                 }
-                
+
                 return Db.TransactionOutcome.COMMIT;
-            });
-        } catch (Error e) {
-            debug("Error populating internaldate_time_t column during upgrade to database schema 12: %s",
-                e.message);
-        }
+            }, cancellable);
     }
-    
+
     // Version 13.
-    private void post_upgrade_populate_additional_attachments() {
-        try {
-            exec_transaction(Db.TransactionType.RW, (cx) => {
+    private async void
+        post_upgrade_populate_additional_attachments(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
                 Db.Statement stmt = cx.prepare("""
                     SELECT id, header, body
                     FROM MessageTable
@@ -334,11 +324,12 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                 stmt.bind_int(0, Geary.Email.REQUIRED_FOR_MESSAGE);
                 stmt.bind_int(1, Geary.Email.REQUIRED_FOR_MESSAGE);
                 Db.Result select = stmt.exec();
+
                 while (!select.finished) {
                     int64 id = select.rowid_at(0);
                     Geary.Memory.Buffer header = select.string_buffer_at(1);
                     Geary.Memory.Buffer body = select.string_buffer_at(2);
-                    
+
                     try {
                         Geary.RFC822.Message message = new Geary.RFC822.Message.from_parts(
                             new RFC822.Header(header), new RFC822.Text(body));
@@ -350,53 +341,52 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                     } catch (Error e) {
                         debug("Error fetching inline Mime parts: %s", e.message);
                     }
-                    
+
                     select.next();
                 }
-                
+
                 // additionally, because this schema change (and code changes as well) introduces
                 // two new types of attachments as well as processing for all MIME text sections
                 // of messages (not just the first one), blow away the search table and let the
                 // search indexer start afresh
                 cx.exec("DELETE FROM MessageSearchTable");
-                
+
                 return Db.TransactionOutcome.COMMIT;
-            });
-        } catch (Error e) {
-            debug("Error populating old inline attachments during upgrade to database schema 13: %s",
-                e.message);
-        }
+            }, cancellable);
     }
-    
+
     // Version 14.
-    private void post_upgrade_expand_page_size() {
-        try {
-            // When the MessageSearchTable is first touched, SQLite seems to
-            // read the whole table into memory (or an awful lot of data,
-            // either way).  This was causing slowness when Geary first started
-            // and checked for any messages not yet in the search table.  With
-            // the database's page_size set to 4096, the reads seem to happen
-            // about 2 orders of magnitude quicker, probably because 4096
-            // matches the default filesystem block size and/or Linux's default
-            // memory page size.  With this set, the full read into memory is
-            // barely noticeable even on slow machines.
-            
-            // NOTE: these can't be in the .sql file itself because they must
-            // be back to back, outside of a transaction.
-            exec("""
-                PRAGMA page_size = 4096;
-                VACUUM;
-            """);
-        } catch (Error e) {
-            debug("Error bumping page_size or vacuuming database; performance may be degraded: %s",
-                e.message);
-        }
+    private async void post_upgrade_expand_page_size(Cancellable? cancellable)
+        throws Error {
+        // When the MessageSearchTable is first touched,
+        // SQLite seems to read the whole table into memory
+        // (or an awful lot of data, either way).  This was
+        // causing slowness when Geary first started and
+        // checked for any messages not yet in the search
+        // table.  With the database's page_size set to 4096,
+        // the reads seem to happen about 2 orders of
+        // magnitude quicker, probably because 4096 matches
+        // the default filesystem block size and/or Linux's
+        // default memory page size.  With this set, the full
+        // read into memory is barely noticeable even on slow
+        // machines.
+
+        // NOTE: these can't be in the .sql file itself because
+        // they must be back to back, outside of a transaction
+        Geary.Db.Connection cx = yield open_connection();
+        yield Nonblocking.Concurrent.global.schedule_async(() => {
+                cx.exec("""
+                    PRAGMA page_size = 4096;
+                    VACUUM;
+                """);
+            }, cancellable);
     }
-    
+
     // Version 15
-    private void post_upgrade_fix_localized_internaldates() {
-        try {
-            exec_transaction(Db.TransactionType.RW, (cx) => {
+    private async void
+        post_upgrade_fix_localized_internaldates(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
                 Db.Statement stmt = cx.prepare("""
                     SELECT id, internaldate, fields
                     FROM MessageTable
@@ -443,19 +433,15 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                     // reuse statment, overwrite invalid_id, fields only
                     stmt.reset(Db.ResetScope.SAVE_BINDINGS);
                 }
-                
+
                 return Db.TransactionOutcome.COMMIT;
-            });
-        } catch (Error err) {
-            debug("Error fixing INTERNALDATES during upgrade to schema 15 for %s: %s",
-                  this.path, err.message);
-        }
+            }, cancellable);
     }
-    
+
     // Version 19.
-    private void post_upgrade_validate_contacts() {
-        try {
-            exec_transaction(Db.TransactionType.RW, (cx) => {
+    private async void post_upgrade_validate_contacts(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
                 Db.Result result = cx.query("SELECT id, email FROM ContactTable");
                 while (!result.finished) {
                     string email = result.string_at(1);
@@ -471,16 +457,13 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                 }
                 
                 return Db.TransactionOutcome.COMMIT;
-            });
-        } catch (Error e) {
-            debug("Error fixing up contacts table: %s", e.message);
-        }
+            }, cancellable);
     }
-    
+
     // Version 22
-    private void post_upgrade_rebuild_attachments() {
-        try {
-            exec_transaction(Db.TransactionType.RW, (cx) => {
+    private async void post_upgrade_rebuild_attachments(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
                 Db.Statement stmt = cx.prepare("""
                     SELECT id, header, body
                     FROM MessageTable
@@ -492,34 +475,33 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                 Db.Result results = stmt.exec();
                 if (results.finished)
                     return Db.TransactionOutcome.ROLLBACK;
-                
+
                 do {
                     int64 message_id = results.rowid_at(0);
                     Geary.Memory.Buffer header = results.string_buffer_at(1);
                     Geary.Memory.Buffer body = results.string_buffer_at(2);
-                    
+
                     Geary.RFC822.Message message;
                     try {
                         message = new Geary.RFC822.Message.from_parts(
                             new RFC822.Header(header), new RFC822.Text(body));
                     } catch (Error err) {
                         debug("Error decoding message: %s", err.message);
-                        
                         continue;
                     }
-                    
+
                     // build a list of attachments in the message itself
-                    Gee.List<GMime.Part> msg_attachments = message.get_attachments();
-                    
-                    // delete all attachments for this message
+                    Gee.List<GMime.Part> msg_attachments =
+                    message.get_attachments();
+
                     try {
                         Geary.ImapDB.Folder.do_delete_attachments(cx, message_id);
                     } catch (Error err) {
-                        debug("Error deleting existing attachments: %s", err.message);
-                        
+                        debug("Error deleting existing attachments: %s",
+                              err.message);
                         continue;
                     }
-                    
+
                     // rebuild all
                     try {
                         Geary.ImapDB.Folder.do_save_attachments_db(cx, message_id, msg_attachments,
@@ -530,35 +512,31 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                         // fallthrough
                     }
                 } while (results.next());
-                
+
                 // rebuild search table due to potentially new attachments
                 cx.exec("DELETE FROM MessageSearchTable");
-                
+
                 return Db.TransactionOutcome.COMMIT;
-            });
-        } catch (Error e) {
-            debug("Error populating old inline attachments during upgrade to database schema 13: %s",
-                e.message);
-        }
+            }, cancellable);
     }
-    
+
     // Version 23
-    private void post_upgrade_add_tokenizer_table() {
-        try {
-            string stemmer = find_appropriate_search_stemmer();
-            debug("Creating tokenizer table using %s stemmer", stemmer);
-            
-            // These can't go in the .sql file because its schema (the stemmer
-            // algorithm) is determined at runtime.
-            exec("""
-                CREATE VIRTUAL TABLE TokenizerTable USING fts3tokenize(
-                    unicodesn,
-                    "stemmer=%s"
-                );
-            """.printf(stemmer));
-        } catch (Error e) {
-            error("Error creating tokenizer table: %s", e.message);
-        }
+    private async void post_upgrade_add_tokenizer_table(Cancellable? cancellable)
+        throws Error {
+        yield exec_transaction_async(Db.TransactionType.RW, (cx) => {
+                string stemmer = find_appropriate_search_stemmer();
+                debug("Creating tokenizer table using %s stemmer", stemmer);
+
+                // These can't go in the .sql file because its schema (the stemmer
+                // algorithm) is determined at runtime.
+                cx.exec("""
+                    CREATE VIRTUAL TABLE TokenizerTable USING fts3tokenize(
+                        unicodesn,
+                        "stemmer=%s"
+                    );
+                """.printf(stemmer));
+                return Db.TransactionOutcome.COMMIT;
+            }, cancellable);
     }
 
     /**
@@ -612,5 +590,5 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
         cx.set_synchronous(Db.SynchronousMode.NORMAL);
         sqlite3_unicodesn_register_tokenizer(cx.db);
     }
-}
 
+}
diff --git a/src/engine/imap-db/imap-db-gc.vala b/src/engine/imap-db/imap-db-gc.vala
index 11ec3b1..b567cb4 100644
--- a/src/engine/imap-db/imap-db-gc.vala
+++ b/src/engine/imap-db/imap-db-gc.vala
@@ -205,9 +205,10 @@ private class Geary.ImapDB.GC {
         
         // NOTE: VACUUM cannot happen inside a transaction, so to avoid blocking the main thread,
         // run a non-transacted command from a background thread
+        Geary.Db.Connection cx = yield db.open_connection(cancellable);
         yield Nonblocking.Concurrent.global.schedule_async(() => {
-            db.open_connection(cancellable).exec("VACUUM", cancellable);
-            
+            cx.exec("VACUUM", cancellable);
+
             // it's a small thing, but take snapshot of time when vacuum completes, as scheduling
             // of the next transaction is not instantaneous
             last_vacuum_time = new DateTime.now_local();
@@ -220,7 +221,7 @@ private class Geary.ImapDB.GC {
         // update last vacuum time and reset messages reaped since last vacuum ... don't allow this
         // to be cancelled, really want to get this in stone so the user doesn't re-vacuum
         // unnecessarily
-        yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
+        yield cx.exec_transaction_async(Db.TransactionType.WO, (cx) => {
             Db.Statement stmt = cx.prepare("""
                 UPDATE GarbageCollectionTable
                 SET last_vacuum_time_t = ?, reaped_messages_since_last_vacuum = ?
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index f1ad286..cd96c03 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -26,9 +26,12 @@ set(TEST_ENGINE_SRC
   engine/app/app-conversation-test.vala
   engine/app/app-conversation-monitor-test.vala
   engine/app/app-conversation-set-test.vala
+  engine/db/db-database-test.vala
+  engine/db/db-versioned-database-test.vala
   engine/imap/command/imap-create-command-test.vala
   engine/imap/response/imap-namespace-response-test.vala
   engine/imap/transport/imap-deserializer-test.vala
+  engine/imap-db/imap-db-database-test.vala
   engine/imap-engine/account-processor-test.vala
   engine/mime-content-type-test.vala
   engine/rfc822-mailbox-address-test.vala
diff --git a/test/engine/db/db-database-test.vala b/test/engine/db/db-database-test.vala
new file mode 100644
index 0000000..e79e8d4
--- /dev/null
+++ b/test/engine/db/db-database-test.vala
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2018 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+class Geary.Db.DatabaseTest : TestCase {
+
+
+    public DatabaseTest() {
+        base("Geary.Db.DatabaseTest");
+        add_test("transient_open", transient_open);
+        add_test("open_existing", open_existing);
+        add_test("open_create_file", open_create_file);
+        add_test("open_create_dir", open_create_dir);
+        add_test("open_create_dir_existing", open_create_dir_existing);
+    }
+
+    public void transient_open() throws Error {
+        Database db = new Geary.Db.Database.transient();
+        db.open.begin(
+            Geary.Db.DatabaseFlags.NONE, null, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        // Need to get a connection since the database doesn't
+        // actually get created until then
+        db.get_master_connection();
+    }
+
+    public void open_existing() throws Error {
+        GLib.FileIOStream stream;
+        GLib.File tmp_file = GLib.File.new_tmp(
+            "geary-db-database-test-XXXXXX", out stream
+        );
+
+        Database db = new Geary.Db.Database.persistent(tmp_file);
+        db.open.begin(
+            Geary.Db.DatabaseFlags.NONE, null, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        // Need to get a connection since the database doesn't
+        // actually get created until then
+        db.get_master_connection();
+
+        tmp_file.delete();
+    }
+
+    public void open_create_file() throws Error {
+        GLib.File tmp_dir = GLib.File.new_for_path(
+            GLib.DirUtils.make_tmp("geary-db-database-test-XXXXXX")
+        );
+
+        Database db = new Geary.Db.Database.persistent(
+            tmp_dir.get_child("test.db")
+        );
+        db.open.begin(
+            Geary.Db.DatabaseFlags.CREATE_FILE, null, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        // Need to get a connection since the database doesn't
+        // actually get created until then
+        db.get_master_connection();
+
+        db.file.delete();
+        tmp_dir.delete();
+    }
+
+    public void open_create_dir() throws Error {
+        GLib.File tmp_dir = GLib.File.new_for_path(
+            GLib.DirUtils.make_tmp("geary-db-database-test-XXXXXX")
+        );
+
+        Database db = new Geary.Db.Database.persistent(
+            tmp_dir.get_child("nonexistent").get_child("test.db")
+        );
+        db.open.begin(
+            Geary.Db.DatabaseFlags.CREATE_DIRECTORY |
+            Geary.Db.DatabaseFlags.CREATE_FILE,
+            null, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        // Need to get a connection since the database doesn't
+        // actually get created until then
+        db.get_master_connection();
+
+        db.file.delete();
+        db.file.get_parent().delete();
+        tmp_dir.delete();
+    }
+
+    public void open_create_dir_existing() throws Error {
+        GLib.File tmp_dir = GLib.File.new_for_path(
+            GLib.DirUtils.make_tmp("geary-db-database-test-XXXXXX")
+        );
+
+        Database db = new Geary.Db.Database.persistent(
+            tmp_dir.get_child("test.db")
+        );
+        db.open.begin(
+            Geary.Db.DatabaseFlags.CREATE_DIRECTORY |
+            Geary.Db.DatabaseFlags.CREATE_FILE,
+            null, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        // Need to get a connection since the database doesn't
+        // actually get created until then
+        db.get_master_connection();
+
+        db.file.delete();
+        tmp_dir.delete();
+    }
+
+
+}
diff --git a/test/engine/db/db-versioned-database-test.vala b/test/engine/db/db-versioned-database-test.vala
new file mode 100644
index 0000000..6d52abc
--- /dev/null
+++ b/test/engine/db/db-versioned-database-test.vala
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2018 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+class Geary.Db.VersionedDatabaseTest : TestCase {
+
+
+    public VersionedDatabaseTest() {
+        base("Geary.Db.VersionedDatabaseTest");
+        add_test("open_new", open_new);
+    }
+
+    public void open_new() throws Error {
+        GLib.File tmp_dir = GLib.File.new_for_path(
+            GLib.DirUtils.make_tmp("geary-db-database-test-XXXXXX")
+        );
+
+        GLib.File sql1 = tmp_dir.get_child("version-001.sql");
+        sql1.create(
+            GLib.FileCreateFlags.NONE
+        ).write("CREATE TABLE TestTable (id INTEGER PRIMARY KEY, col TEXT);".data);
+
+        GLib.File sql2 = tmp_dir.get_child("version-002.sql");
+        sql2.create(
+            GLib.FileCreateFlags.NONE
+        ).write("INSERT INTO TestTable (col) VALUES ('value');".data);
+
+        VersionedDatabase db = new VersionedDatabase.persistent(
+            tmp_dir.get_child("test.db"), tmp_dir
+        );
+
+        db.open.begin(
+            Geary.Db.DatabaseFlags.CREATE_FILE, null, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        Geary.Db.Result result = db.query("SELECT * FROM TestTable;");
+        assert_false(result.finished, "Row not inserted");
+        assert_string("value", result.string_for("col"));
+        assert_false(result.next(), "Multiple rows inserted");
+
+        db.file.delete();
+        sql1.delete();
+        sql2.delete();
+        tmp_dir.delete();
+    }
+
+
+}
diff --git a/test/engine/imap-db/imap-db-database-test.vala b/test/engine/imap-db/imap-db-database-test.vala
new file mode 100644
index 0000000..209694a
--- /dev/null
+++ b/test/engine/imap-db/imap-db-database-test.vala
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2018 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+class Geary.ImapDB.DatabaseTest : TestCase {
+
+
+    public DatabaseTest() {
+        base("Geary.ImapDb.DatabaseTest");
+        add_test("open_new", open_new);
+    }
+
+    public void open_new() throws Error {
+        GLib.File tmp_dir = GLib.File.new_for_path(
+            GLib.DirUtils.make_tmp("geary-db-database-test-XXXXXX")
+        );
+
+        Database db = new Database(
+            tmp_dir,
+            GLib.File.new_for_path(_SOURCE_ROOT_DIR).get_child("sql"),
+            new Geary.SimpleProgressMonitor(Geary.ProgressType.DB_UPGRADE),
+            new Geary.SimpleProgressMonitor(Geary.ProgressType.DB_VACUUM),
+            "test example com"
+        );
+
+        db.open.begin(
+            Geary.Db.DatabaseFlags.CREATE_FILE, null,
+            (obj, ret) => { async_complete(ret); }
+        );
+        db.open.end(async_result());
+
+        // Need to get a connection since the database doesn't
+        // actually get created until then
+        db.get_master_connection();
+
+        // Need to close it again to stop the GC process running
+        db.close();
+
+        db.file.delete();
+        tmp_dir.delete();
+    }
+
+
+}
diff --git a/test/meson.build b/test/meson.build
index b844d0d..f2e5f44 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -22,9 +22,12 @@ geary_test_engine_sources = [
   'engine/app/app-conversation-test.vala',
   'engine/app/app-conversation-monitor-test.vala',
   'engine/app/app-conversation-set-test.vala',
+  'engine/db/db-database-test.vala',
+  'engine/db/db-versioned-database-test.vala',
   'engine/imap/command/imap-create-command-test.vala',
   'engine/imap/response/imap-namespace-response-test.vala',
   'engine/imap/transport/imap-deserializer-test.vala',
+  'engine/imap-db/imap-db-database-test.vala',
   'engine/imap-engine/account-processor-test.vala',
   'engine/mime-content-type-test.vala',
   'engine/rfc822-mailbox-address-test.vala',
diff --git a/test/test-engine.vala b/test/test-engine.vala
index ab4a0ef..071f781 100644
--- a/test/test-engine.vala
+++ b/test/test-engine.vala
@@ -30,10 +30,13 @@ int main(string[] args) {
     engine.add_suite(new Geary.App.ConversationSetTest().get_suite());
     // Depends on ConversationTest and ConversationSetTest passing
     engine.add_suite(new Geary.App.ConversationMonitorTest().get_suite());
+    engine.add_suite(new Geary.Db.DatabaseTest().get_suite());
+    engine.add_suite(new Geary.Db.VersionedDatabaseTest().get_suite());
     engine.add_suite(new Geary.HTML.UtilTest().get_suite());
     engine.add_suite(new Geary.Imap.DeserializerTest().get_suite());
     engine.add_suite(new Geary.Imap.CreateCommandTest().get_suite());
     engine.add_suite(new Geary.Imap.NamespaceResponseTest().get_suite());
+    engine.add_suite(new Geary.ImapDB.DatabaseTest().get_suite());
     engine.add_suite(new Geary.ImapEngine.AccountProcessorTest().get_suite());
     engine.add_suite(new Geary.Inet.Test().get_suite());
     engine.add_suite(new Geary.JS.Test().get_suite());


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]