[geary/mjog/921-db-locked: 3/7] Geary.Db.Connection: Split up into db and transaction-specific impls




commit 257f6fb90151eed2cc16bfe5ce331603f5f72910
Author: Michael Gratton <mike vee net>
Date:   Thu Sep 3 16:54:30 2020 +1000

    Geary.Db.Connection: Split up into db and transaction-specific impls
    
    Convert `Connection` into an interface, add two concrete implementations
    that allow splitting up the database connection used generally, and the
    connection passed to transactions. This allows limiting the API surface
    that transactions have access to (so they can't e.g. create
    sub-transactions) and perform transaction-specific work (e.g. better
    logging when an error occurs).

 po/POTFILES.in                               |   2 +
 src/engine/db/db-connection.vala             | 316 ++++++++-------------------
 src/engine/db/db-database-connection.vala    | 247 +++++++++++++++++++++
 src/engine/db/db-database.vala               |  74 ++++---
 src/engine/db/db-transaction-async-job.vala  |   8 +-
 src/engine/db/db-transaction-connection.vala |  65 ++++++
 src/engine/db/db-versioned-database.vala     |   4 +-
 src/engine/db/db.vala                        |   5 +-
 src/engine/imap-db/imap-db-database.vala     |   6 +-
 src/engine/imap-db/imap-db-gc.vala           |   2 +-
 src/engine/meson.build                       |   2 +
 11 files changed, 461 insertions(+), 270 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 404523c71..09c3c9704 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -213,12 +213,14 @@ src/engine/common/common-message-data.vala
 src/engine/db/db.vala
 src/engine/db/db-connection.vala
 src/engine/db/db-context.vala
+src/engine/db/db-database-connection.vala
 src/engine/db/db-database-error.vala
 src/engine/db/db-database.vala
 src/engine/db/db-result.vala
 src/engine/db/db-statement.vala
 src/engine/db/db-synchronous-mode.vala
 src/engine/db/db-transaction-async-job.vala
+src/engine/db/db-transaction-connection.vala
 src/engine/db/db-transaction-outcome.vala
 src/engine/db/db-transaction-type.vala
 src/engine/db/db-versioned-database.vala
diff --git a/src/engine/db/db-connection.vala b/src/engine/db/db-connection.vala
index f429b3566..4f0859e11 100644
--- a/src/engine/db/db-connection.vala
+++ b/src/engine/db/db-connection.vala
@@ -1,34 +1,24 @@
 /*
- * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2020 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.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
 /**
- * A Connection represents a connection to an open database.
+ * Represents a connection to an opened 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.
+ * Connections are associated with a specific {@link Database}
+ * instance. Because SQLite uses a synchronous interface, all calls on
+ * a single connection instance are blocking. Use multiple connections
+ * for concurrent access to a single database, or use the asynchronous
+ * transaction support provided by {@link Database}.
  *
- * Connections are associated with a Database.  Use
- * Database.open_connection() to create one.
- *
- * A Connection will close when its last reference is dropped.
+ * A connection will be automatically closed when its last reference
+ * is dropped.
  */
-
-public class Geary.Db.Connection : Geary.Db.Context {
-    /**
-     * Default value is for *no* timeout, that is, the Sqlite will not retry BUSY results.
-     */
-    public const int DEFAULT_BUSY_TIMEOUT_MSEC = 0;
-
-    /**
-     * This value gives a generous amount of time for SQLite to finish a big write operation and
-     * relinquish the lock to other waiting transactions.
-     */
-    public const int RECOMMENDED_BUSY_TIMEOUT_MSEC = 60 * 1000;
+public interface Geary.Db.Connection : Context {
 
     private const string PRAGMA_FOREIGN_KEYS = "foreign_keys";
     private const string PRAGMA_RECURSIVE_TRIGGERS = "recursive_triggers";
@@ -40,140 +30,34 @@ public class Geary.Db.Connection : Geary.Db.Context {
     private const string PRAGMA_PAGE_COUNT = "page_count";
     private const string PRAGMA_PAGE_SIZE = "page_size";
 
-    // this is used for logging purposes only; connection numbers mean nothing to SQLite
-    private static int next_cx_number = 0;
 
     /**
      * See [[http://www.sqlite.org/c3ref/last_insert_rowid.html]]
      */
     public int64 last_insert_rowid { get {
-        return db.last_insert_rowid();
+        return this.db.last_insert_rowid();
     } }
 
     /**
      * See [[http://www.sqlite.org/c3ref/changes.html]]
      */
     public int last_modified_rows { get {
-        return db.changes();
+        return this.db.changes();
     } }
 
     /**
      * See [[http://www.sqlite.org/c3ref/total_changes.html]]
      */
     public int total_modified_rows { get {
-        return db.total_changes();
+        return this.db.total_changes();
     } }
 
-    public weak Database database { get; private set; }
-
-    internal Sqlite.Database db;
+    /** The database this connection is associated with. */
+    public abstract Database database { get; }
 
-    private int cx_number;
-    private int busy_timeout_msec = DEFAULT_BUSY_TIMEOUT_MSEC;
+    /** The underlying SQLite database connection. */
+    internal abstract Sqlite.Database db { get; }
 
-    internal Connection(Database database, int sqlite_flags, Cancellable? cancellable) throws Error {
-        this.database = database;
-
-        lock (next_cx_number) {
-            cx_number = next_cx_number++;
-        }
-
-        check_cancelled("Connection.ctor", cancellable);
-
-        try {
-            throw_on_error(
-                "Connection.ctor",
-                Sqlite.Database.open_v2(database.path, out db, sqlite_flags, null)
-            );
-        } catch (DatabaseError derr) {
-            // don't throw BUSY error for open unless no db object was returned, as it's possible for
-            // open_v2() to return an error *and* a valid Database object, see:
-            // http://www.sqlite.org/c3ref/open.html
-            if (!(derr is DatabaseError.BUSY) || (db == null))
-                throw derr;
-        }
-    }
-
-    /**
-     * Execute a plain text SQL statement.  More than one SQL statement may be in the string.  See
-     * [[http://www.sqlite.org/lang.html]] for more information on SQLite's SQL syntax.
-     *
-     * There is no way to retrieve a result iterator from this call.
-     *
-     * 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]]
-     */
-    public void exec(string sql, Cancellable? cancellable = null) throws Error {
-        if (Db.Context.enable_sql_logging) {
-            debug("exec:\n\t%s", sql);
-        }
-
-        check_cancelled("Connection.exec", cancellable);
-        throw_on_error("Connection.exec", db.exec(sql), sql);
-    }
-
-    /**
-     * Loads a text file of SQL commands into memory and executes them at once with exec().
-     *
-     * There is no way to retrieve a result iterator from this call.
-     *
-     * 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);
-
-        string sql;
-        FileUtils.get_contents(file.get_path(), out sql);
-
-        exec(sql, cancellable);
-    }
-
-    /**
-     * Executes a plain text SQL statement and returns a Result object directly.
-     * This call creates an intermediate Statement object which may be fetched from Result.statement.
-     */
-    public Result query(string sql, Cancellable? cancellable = null) throws Error {
-        return (new Statement(this, sql)).exec(cancellable);
-    }
-
-    /**
-     * Prepares a Statement which may have values bound to it and executed.  See
-     * [[http://www.sqlite.org/c3ref/prepare.html]]
-     */
-    public Statement prepare(string sql) throws DatabaseError {
-        return new Statement(this, sql);
-    }
-
-    /**
-     * See set_busy_timeout_msec().
-     */
-    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.
-     *
-     * 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 {
-        if (this.busy_timeout_msec == busy_timeout_msec)
-            return;
-
-        throw_on_error("Database.set_busy_timeout", db.busy_timeout(busy_timeout_msec));
-        this.busy_timeout_msec = busy_timeout_msec;
-    }
 
     /**
      * Returns the result of a PRAGMA as a boolean.  See [[http://www.sqlite.org/pragma.html]]
@@ -181,7 +65,7 @@ public class Geary.Db.Connection : Geary.Db.Context {
      * Note that if the PRAGMA does not return a boolean, the results are undefined.  A boolean
      * in SQLite, however, includes 1 and 0, so an integer may be mistaken as a boolean.
      */
-    public bool get_pragma_bool(string name) throws Error {
+    public bool get_pragma_bool(string name) throws GLib.Error {
         string response = query("PRAGMA %s".printf(name)).nonnull_string_at(0);
         switch (response.down()) {
             case "1":
@@ -207,7 +91,7 @@ public class Geary.Db.Connection : Geary.Db.Context {
     /**
      * Sets a boolean PRAGMA value to either "true" or "false".
      */
-    public void set_pragma_bool(string name, bool b) throws Error {
+    public void set_pragma_bool(string name, bool b) throws GLib.Error {
         exec("PRAGMA %s=%s".printf(name, b ? "true" : "false"));
     }
 
@@ -218,14 +102,14 @@ public class Geary.Db.Connection : Geary.Db.Context {
      * boolean in SQLite includes 1 and 0, it's possible for those values to be converted to an
      * integer.
      */
-    public int get_pragma_int(string name) throws Error {
+    public int get_pragma_int(string name) throws GLib.Error {
         return query("PRAGMA %s".printf(name)).int_at(0);
     }
 
     /**
      * Sets an integer PRAGMA value.
      */
-    public void set_pragma_int(string name, int d) throws Error {
+    public void set_pragma_int(string name, int d) throws GLib.Error {
         exec("PRAGMA %s=%d".printf(name, d));
     }
 
@@ -236,28 +120,28 @@ public class Geary.Db.Connection : Geary.Db.Context {
      * boolean in SQLite includes 1 and 0, it's possible for those values to be converted to an
      * integer.
      */
-    public int64 get_pragma_int64(string name) throws Error {
+    public int64 get_pragma_int64(string name) throws GLib.Error {
         return query("PRAGMA %s".printf(name)).int64_at(0);
     }
 
     /**
      * Sets a 64-bit integer PRAGMA value.
      */
-    public void set_pragma_int64(string name, int64 ld) throws Error {
+    public void set_pragma_int64(string name, int64 ld) throws GLib.Error {
         exec("PRAGMA %s=%s".printf(name, ld.to_string()));
     }
 
     /**
      * Returns the result of a PRAGMA as a string.  See [[http://www.sqlite.org/pragma.html]]
      */
-    public string get_pragma_string(string name) throws Error {
+    public string get_pragma_string(string name) throws GLib.Error {
         return query("PRAGMA %s".printf(name)).nonnull_string_at(0);
     }
 
     /**
      * Sets a string PRAGMA value.
      */
-    public void set_pragma_string(string name, string str) throws Error {
+    public void set_pragma_string(string name, string str) throws GLib.Error {
         exec("PRAGMA %s=%s".printf(name, str));
     }
 
@@ -268,7 +152,7 @@ public class Geary.Db.Connection : Geary.Db.Context {
      *
      * @see set_user_version_number
      */
-    public int get_user_version_number() throws Error {
+    public int get_user_version_number() throws GLib.Error {
         return get_pragma_int(PRAGMA_USER_VERSION);
     }
 
@@ -278,7 +162,7 @@ public class Geary.Db.Connection : Geary.Db.Context {
      *
      * See [[http://www.sqlite.org/pragma.html#pragma_schema_version]]
      */
-    public void set_user_version_number(int version) throws Error {
+    public void set_user_version_number(int version) throws GLib.Error {
         set_pragma_int(PRAGMA_USER_VERSION, version);
     }
 
@@ -288,170 +172,142 @@ public class Geary.Db.Connection : Geary.Db.Context {
      *
      * Since this number is maintained by SQLite, Geary.Db doesn't offer a way to set it.
      */
-    public int get_schema_version_number() throws Error {
+    public int get_schema_version_number() throws GLib.Error {
         return get_pragma_int(PRAGMA_SCHEMA_VERSION);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_foreign_keys]]
      */
-    public void set_foreign_keys(bool enabled) throws Error {
+    public void set_foreign_keys(bool enabled) throws GLib.Error {
         set_pragma_bool(PRAGMA_FOREIGN_KEYS, enabled);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_foreign_keys]]
      */
-    public bool get_foreign_keys() throws Error {
+    public bool get_foreign_keys() throws GLib.Error {
         return get_pragma_bool(PRAGMA_FOREIGN_KEYS);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_recursive_triggers]]
      */
-    public void set_recursive_triggers(bool enabled) throws Error {
+    public void set_recursive_triggers(bool enabled) throws GLib.Error {
         set_pragma_bool(PRAGMA_RECURSIVE_TRIGGERS, enabled);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_recursive_triggers]]
      */
-    public bool get_recursive_triggers() throws Error {
+    public bool get_recursive_triggers() throws GLib.Error {
         return get_pragma_bool(PRAGMA_RECURSIVE_TRIGGERS);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_secure_delete]]
      */
-    public void set_secure_delete(bool enabled) throws Error {
+    public void set_secure_delete(bool enabled) throws GLib.Error {
         set_pragma_bool(PRAGMA_SECURE_DELETE, enabled);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_secure_delete]]
      */
-    public bool get_secure_delete() throws Error {
+    public bool get_secure_delete() throws GLib.Error {
         return get_pragma_bool(PRAGMA_SECURE_DELETE);
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_synchronous]]
      */
-    public void set_synchronous(SynchronousMode mode) throws Error {
+    public void set_synchronous(SynchronousMode mode) throws GLib.Error {
         set_pragma_string(PRAGMA_SYNCHRONOUS, mode.sql());
     }
 
     /**
      * See [[http://www.sqlite.org/pragma.html#pragma_synchronous]]
      */
-    public SynchronousMode get_synchronous() throws Error {
+    public SynchronousMode get_synchronous() throws GLib.Error {
         return SynchronousMode.parse(get_pragma_string(PRAGMA_SYNCHRONOUS));
     }
 
     /**
      * See [[https://www.sqlite.org/pragma.html#pragma_freelist_count]]
      */
-    public int64 get_free_page_count() throws Error {
+    public int64 get_free_page_count() throws GLib.Error {
         return get_pragma_int64(PRAGMA_FREELIST_COUNT);
     }
 
     /**
      * See [[https://www.sqlite.org/pragma.html#pragma_page_count]]
      */
-    public int64 get_total_page_count() throws Error {
+    public int64 get_total_page_count() throws GLib.Error {
         return get_pragma_int64(PRAGMA_PAGE_COUNT);
     }
 
     /**
      * See [[https://www.sqlite.org/pragma.html#pragma_page_size]]
      */
-    public int get_page_size() throws Error {
+    public int get_page_size() throws GLib.Error {
         return get_pragma_int(PRAGMA_PAGE_SIZE);
     }
 
     /**
-     * Executes one or more queries inside an SQLite transaction.  This call will initiate a
-     * transaction according to the TransactionType specified (although this is merely an
-     * optimization -- no matter the transaction type, SQLite guarantees the subsequent operations
-     * to be atomic).  The commands executed inside the TransactionMethod against the
-     * supplied Db.Connection will be in the context of the transaction.  If the TransactionMethod
-     * returns TransactionOutcome.COMMIT, the transaction will be committed to the database,
-     * otherwise it will be rolled back and the database left unchanged.
+     * Prepares a single SQL statement for execution.
      *
-     * It's inadvisable to call exec_transaction() inside exec_transaction().  SQLite has a notion
-     * of savepoints that allow for nested transactions; they are not currently supported.
+     * Only a single SQL statement may be included in the string. See
+     * [[http://www.sqlite.org/lang.html]] for more information on
+     * SQLite's SQL syntax.
      *
-     * See [[http://www.sqlite.org/lang_transaction.html]]
+     * The given SQL string may contain placeholders for values, which
+     * must then be bound with actual values by calls such as {@link
+     * Statement.bind_string} prior to executing.
+     *
+     * SQLite reference: [[http://www.sqlite.org/c3ref/prepare.html]]
      */
-    public TransactionOutcome exec_transaction(TransactionType type, TransactionMethod cb,
-        Cancellable? cancellable = null) throws Error {
-        // initiate the transaction
-        try {
-            exec(type.sql(), cancellable);
-        } catch (Error err) {
-            if (!(err is IOError.CANCELLED))
-                debug("Connection.exec_transaction: unable to %s: %s", type.sql(), err.message);
-
-            throw err;
-        }
-
-        // If transaction throws an Error, must rollback, always
-        TransactionOutcome outcome = TransactionOutcome.ROLLBACK;
-        Error? caught_err = null;
-        try {
-            // perform the transaction
-            outcome = cb(this, cancellable);
-        } catch (Error err) {
-            if (!(err is IOError.CANCELLED))
-                debug("Connection.exec_transaction: transaction threw error: %s", err.message);
-
-            caught_err = err;
-        }
+    public abstract Statement prepare(string sql)
+        throws DatabaseError;
 
-        // commit/rollback ... don't use Cancellable for TransactionOutcome because it's SQL *must*
-        // execute in order to unlock the database
-        try {
-            exec(outcome.sql());
-        } catch (Error err) {
-            debug("Connection.exec_transaction: Unable to %s transaction: %s", outcome.to_string(),
-                err.message);
-        }
-
-        if (caught_err != null)
-            throw caught_err;
-
-        return outcome;
-    }
+    /**
+     * Executes a single SQL statement, returning a result.
+     *
+     * Only a single SQL statement may be included in the string. See
+     * [[http://www.sqlite.org/lang.html]] for more information on
+     * SQLite's SQL syntax.
+     *
+     * @see exec
+     */
+    public abstract Result query(string sql, GLib.Cancellable? cancellable = null)
+        throws GLib.Error;
 
     /**
-     * Starts a new asynchronous transaction for this connection.
+     * Executes or more SQL statements without returning a result.
+     *
+     * More than one SQL statement may be in the string. See
+     * [[http://www.sqlite.org/lang.html]] for more information on
+     * SQLite's SQL syntax.
      *
-     * 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.
+     * There is no way to retrieve a result iterator from this
+     * call. If needed, use {@link query} instead.
      *
-     * Throws {@link DatabaseError.OPEN_REQUIRED} if not open.
+     * SQLite reference: [[http://www.sqlite.org/c3ref/exec.html]]
      */
-    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 abstract void exec(string sql, GLib.Cancellable? cancellable = null)
+        throws GLib.Error;
 
-    public override Connection? get_connection() {
-        return this;
-    }
+    /**
+     * Executes SQL commands from a plain text file.
+     *
+     * The given file is read into memory and executed via a single
+     * call to {@link exec}.
+     *
+     * There is no way to retrieve a result iterator from this call.
+     *
+     * @see Connection.exec
+     */
+    public abstract void exec_file(GLib.File file,
+                                   GLib.Cancellable? cancellable = null)
+        throws GLib.Error;
 
-    public string to_string() {
-        return "[%d] %s".printf(cx_number, database.path);
-    }
 }
diff --git a/src/engine/db/db-database-connection.vala b/src/engine/db/db-database-connection.vala
new file mode 100644
index 000000000..da9f7e7fb
--- /dev/null
+++ b/src/engine/db/db-database-connection.vala
@@ -0,0 +1,247 @@
+/*
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2020 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 primary connection to the database.
+ */
+public class Geary.Db.DatabaseConnection : Context, Connection {
+
+
+    /**
+     * Default value is for the connection's busy timeout.
+     *
+     * By default, SQLite will not retry BUSY results.
+     *
+     * @see busy_timeout
+     */
+    public const int DEFAULT_BUSY_TIMEOUT_MSEC = 0;
+
+    /**
+     * Recommended value is for the connection's busy timeout.
+     *
+     * This value gives a generous amount of time for SQLite to finish
+     * a big write operation and relinquish the lock to other waiting
+     * transactions.
+     *
+     * @see busy_timeout
+     */
+    public const int RECOMMENDED_BUSY_TIMEOUT_MSEC = 60 * 1000;
+
+
+    // This is used for logging purposes only; connection numbers mean
+    // nothing to SQLite
+    private static uint next_cx_number = 0;
+
+
+    /**
+     * The busy timeout for this connection.
+     *
+     * A non-zero, positive value indicates that all operations that
+     * SQLite returns BUSY will be retried until they complete with
+     * success or error. Only after the given amount of time has
+     * transpired will a {@link DatabaseError.BUSY} will be thrown. If
+     * zero or negative, a {@link DatabaseError.BUSY} will be
+     * immediately if the database is already locked when a new lock
+     * is required.
+     *
+     * Setting a positive value imperative for transactions, otherwise
+     * those calls will throw a {@link DatabaseError.BUSY} error
+     * immediately if another transaction has acquired the reserved or
+     * exclusive locks.
+     *
+     * @see DEFAULT_BUSY_TIMEOUT_MSEC
+     * @see RECOMMENDED_BUSY_TIMEOUT_MSEC
+     * @see set_busy_timeout_msec
+     */
+    public int busy_timeout {
+        get; private set; default = DEFAULT_BUSY_TIMEOUT_MSEC;
+    }
+
+    /** {@inheritDoc} */
+    public Database database { get { return this._database; } }
+    private weak Database _database;
+
+    /** {@inheritDoc} */
+    internal Sqlite.Database db { get { return this._db; } }
+    private Sqlite.Database _db;
+
+    private uint cx_number;
+
+
+    internal DatabaseConnection(Database database,
+                                int sqlite_flags,
+                                GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        this._database = database;
+
+        lock (next_cx_number) {
+            this.cx_number = next_cx_number++;
+        }
+
+        check_cancelled("Connection.ctor", cancellable);
+
+        try {
+            throw_on_error(
+                "Connection.ctor",
+                Sqlite.Database.open_v2(
+                    database.path, out this._db, sqlite_flags, null
+                )
+            );
+        } catch (DatabaseError derr) {
+            // don't throw BUSY error for open unless no db object was returned, as it's possible for
+            // open_v2() to return an error *and* a valid Database object, see:
+            // http://www.sqlite.org/c3ref/open.html
+            if (!(derr is DatabaseError.BUSY) || (db == null))
+                throw derr;
+        }
+    }
+
+    /**
+     * Sets the connection's busy timeout in milliseconds.
+     *
+     * @see busy_timeout
+     */
+    public void set_busy_timeout_msec(int timeout_msec) throws GLib.Error {
+        if (this.busy_timeout != timeout_msec) {
+            throw_on_error(
+                "Database.set_busy_timeout",
+                this.db.busy_timeout(timeout_msec)
+            );
+            this.busy_timeout = timeout_msec;
+        }
+    }
+
+    /** {@inheritDoc} */
+    public Statement prepare(string sql) throws DatabaseError {
+        return new Statement(this, sql);
+    }
+
+    /** {@inheritDoc} */
+    public Result query(string sql, GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        return (new Statement(this, sql)).exec(cancellable);
+    }
+
+    /** {@inheritDoc} */
+    public void exec(string sql, GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        if (Db.Context.enable_sql_logging) {
+            debug("exec:\n\t%s", sql);
+        }
+
+        check_cancelled("Connection.exec", cancellable);
+        throw_on_error("Connection.exec", db.exec(sql), sql);
+    }
+
+    /** {@inheritDoc} */
+    public void exec_file(GLib.File file, GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        check_cancelled("Connection.exec_file", cancellable);
+
+        string sql;
+        FileUtils.get_contents(file.get_path(), out sql);
+
+        exec(sql, cancellable);
+    }
+
+    /**
+     * Executes a transaction using this connection.
+     *
+     * Executes one or more queries inside an SQLite transaction.
+     * This call will initiate a transaction according to the
+     * TransactionType specified (although this is merely an
+     * optimization -- no matter the transaction type, SQLite
+     * guarantees the subsequent operations to be atomic).  The
+     * commands executed inside the TransactionMethod against the
+     * supplied Db.Connection will be in the context of the
+     * transaction.  If the TransactionMethod returns
+     * TransactionOutcome.COMMIT, the transaction will be committed to
+     * the database, otherwise it will be rolled back and the database
+     * left unchanged.
+     *
+     * See [[http://www.sqlite.org/lang_transaction.html]]
+     */
+    public TransactionOutcome exec_transaction(TransactionType type,
+                                               TransactionMethod cb,
+                                               GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        var txn_cx = new TransactionConnection(this);
+
+        // initiate the transaction
+        try {
+            txn_cx.exec(type.sql(), cancellable);
+        } catch (GLib.Error err) {
+            if (!(err is GLib.IOError.CANCELLED))
+                debug("Connection.exec_transaction: unable to %s: %s", type.sql(), err.message);
+
+            throw err;
+        }
+
+        // If transaction throws an Error, must rollback, always
+        TransactionOutcome outcome = TransactionOutcome.ROLLBACK;
+        Error? caught_err = null;
+        try {
+            // perform the transaction
+            outcome = cb(txn_cx, cancellable);
+        } catch (GLib.Error err) {
+            if (!(err is GLib.IOError.CANCELLED))
+                debug("Connection.exec_transaction: transaction threw error: %s", err.message);
+
+            caught_err = err;
+        }
+
+        // commit/rollback ... don't use  GLib.Cancellable for
+        // TransactionOutcome because it's SQL *must* execute in order
+        // to unlock the database
+        try {
+            txn_cx.exec(outcome.sql());
+        } catch (GLib.Error err) {
+            debug("Connection.exec_transaction: Unable to %s transaction: %s", outcome.to_string(),
+                err.message);
+        }
+
+        if (caught_err != null) {
+            throw caught_err;
+        }
+
+        return outcome;
+    }
+
+    /**
+     * Executes an asynchronous transaction using 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,
+                                                           GLib.Cancellable? cancellable)
+        throws GLib.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;
+    }
+
+    public string to_string() {
+        return "[%u] %s".printf(this.cx_number, this._database.path);
+    }
+
+}
diff --git a/src/engine/db/db-database.vala b/src/engine/db/db-database.vala
index e28180a47..ac201ef03 100644
--- a/src/engine/db/db-database.vala
+++ b/src/engine/db/db-database.vala
@@ -9,17 +9,12 @@
 /**
  * Represents a single SQLite database.
  *
- * 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 ''primary'' connection.
- *
- * 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}.
+ * This class provides convenience methods to execute queries for
+ * applications that do not require concurrent access to the database,
+ * and it supports executing and asynchronous transaction using a
+ * thread pool, as well as allowing multiple connections to be opened
+ * for fully concurrent access.
  */
-
 public class Geary.Db.Database : Geary.Db.Context {
 
 
@@ -62,7 +57,7 @@ public class Geary.Db.Database : Geary.Db.Context {
         }
     }
 
-    private Connection? primary = null;
+    private DatabaseConnection? primary = null;
     private int outstanding_async_jobs = 0;
     private ThreadPool<TransactionAsyncJob>? thread_pool = null;
 
@@ -145,7 +140,9 @@ public class Geary.Db.Database : Geary.Db.Context {
         // TODO: Allow the caller to specify the name of the test table, so we're not clobbering
         // theirs (however improbable it is to name a table "CorruptionCheckTable")
         if ((flags & DatabaseFlags.READ_ONLY) == 0) {
-            Connection cx = new Connection(this, Sqlite.OPEN_READWRITE, cancellable);
+            var cx = new DatabaseConnection(
+                this, Sqlite.OPEN_READWRITE, cancellable
+            );
 
             try {
                 // drop existing test table (in case created in prior failed open)
@@ -206,17 +203,18 @@ public class Geary.Db.Database : Geary.Db.Context {
     /**
      * Throws DatabaseError.OPEN_REQUIRED if not open.
      */
-    public async Connection open_connection(Cancellable? cancellable = null)
-        throws Error {
-        Connection? cx = null;
+    public async DatabaseConnection
+        open_connection(GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        DatabaseConnection? cx = null;
         yield Nonblocking.Concurrent.global.schedule_async(() => {
                 cx = internal_open_connection(false, cancellable);
             }, cancellable);
         return cx;
     }
 
-    private Connection internal_open_connection(bool is_primary,
-                                                GLib.Cancellable? cancellable)
+    private DatabaseConnection internal_open_connection(bool is_primary,
+                                                        GLib.Cancellable? cancellable)
         throws GLib.Error {
         check_open();
 
@@ -231,7 +229,9 @@ public class Geary.Db.Database : Geary.Db.Context {
             sqlite_flags |= SQLITE_OPEN_URI;
         }
 
-        Connection cx = new Connection(this, sqlite_flags, cancellable);
+        DatabaseConnection cx = new DatabaseConnection(
+            this, sqlite_flags, cancellable
+        );
         prepare_connection(cx);
         return cx;
     }
@@ -247,7 +247,7 @@ public class Geary.Db.Database : Geary.Db.Context {
      *
      * Throws {@link DatabaseError.OPEN_REQUIRED} if not open.
      */
-    public Connection get_primary_connection() throws GLib.Error {
+    public DatabaseConnection get_primary_connection() throws GLib.Error {
         if (this.primary == null)
             this.primary = internal_open_connection(true, null);
 
@@ -261,6 +261,8 @@ public class Geary.Db.Database : Geary.Db.Context {
      * Connection.exec} on the connection returned by {@link
      * get_primary_connection}. Throws {@link
      * DatabaseError.OPEN_REQUIRED} if not open.
+     *
+     * @see Connection.exec
      */
     public void exec(string sql, GLib.Cancellable? cancellable = null)
         throws GLib.Error {
@@ -274,6 +276,8 @@ public class Geary.Db.Database : Geary.Db.Context {
      * Connection.exec_file} on the connection returned by {@link
      * get_primary_connection}. Throws {@link
      * DatabaseError.OPEN_REQUIRED} if not open.
+     *
+     * @see Connection.exec_file
      */
     public void exec_file(File file, GLib.Cancellable? cancellable = null)
         throws GLib.Error {
@@ -287,6 +291,8 @@ public class Geary.Db.Database : Geary.Db.Context {
      * Connection.prepare} on the connection returned by {@link
      * get_primary_connection}. Throws {@link
      * DatabaseError.OPEN_REQUIRED} if not open.
+     *
+     * @see Connection.prepare
      */
     public Statement prepare(string sql) throws GLib.Error {
         return get_primary_connection().prepare(sql);
@@ -299,6 +305,8 @@ public class Geary.Db.Database : Geary.Db.Context {
      * Connection.query} on the connection returned by {@link
      * get_primary_connection}. Throws {@link
      * DatabaseError.OPEN_REQUIRED} if not open.
+     *
+     * @see Connection.query
      */
     public Result query(string sql, GLib.Cancellable? cancellable = null)
         throws GLib.Error {
@@ -309,9 +317,11 @@ public class Geary.Db.Database : Geary.Db.Context {
      * Executes a transaction using the primary connection.
      *
      * This is a convenience method for calling {@link
-     * Connection.exec_transaction} on the connection returned by
-     * {@link get_primary_connection}. Throws {@link
+     * DatabaseConnection.exec_transaction} on the connection returned
+     * by {@link get_primary_connection}. Throws {@link
      * DatabaseError.OPEN_REQUIRED} if not open.
+     *
+     * @see DatabaseConnection.exec_transaction
      */
     public TransactionOutcome exec_transaction(TransactionType type,
                                                TransactionMethod cb,
@@ -325,12 +335,14 @@ public class Geary.Db.Database : Geary.Db.Context {
      *
      * 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.
+     * calls {@link DatabaseConnection.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.
+     *
+     * @see DatabaseConnection.exec_transaction
      */
     public async TransactionOutcome exec_transaction_async(TransactionType type,
                                                            TransactionMethod cb,
@@ -367,15 +379,16 @@ public class Geary.Db.Database : Geary.Db.Context {
      * established connections before being used, such as setting
      * pragmas, custom collation functions, and so on,
      */
-    protected virtual void prepare_connection(Connection cx) throws GLib.Error {
+    protected virtual void prepare_connection(DatabaseConnection cx)
+        throws GLib.Error {
         // No-op by default;
     }
 
     // This method must be thread-safe.
     private void on_async_job(owned TransactionAsyncJob job) {
         // *never* use primary connection for threaded operations
-        Connection? cx = job.cx;
-        Error? open_err = null;
+        var cx = job.default_cx;
+        GLib.Error? open_err = null;
         if (cx == null) {
             try {
                 cx = internal_open_connection(false, job.cancellable);
@@ -386,10 +399,11 @@ public class Geary.Db.Database : Geary.Db.Context {
             }
         }
 
-        if (cx != null)
+        if (cx != null) {
             job.execute(cx);
-        else
+        } else {
             job.failed(open_err);
+        }
 
         lock (outstanding_async_jobs) {
             assert(outstanding_async_jobs > 0);
diff --git a/src/engine/db/db-transaction-async-job.vala b/src/engine/db/db-transaction-async-job.vala
index 9ab8d6b5d..1c242fcda 100644
--- a/src/engine/db/db-transaction-async-job.vala
+++ b/src/engine/db/db-transaction-async-job.vala
@@ -7,7 +7,7 @@
 
 private class Geary.Db.TransactionAsyncJob : BaseObject {
 
-    internal Connection? cx { get; private set; default = null; }
+    internal DatabaseConnection? default_cx { get; private set; }
     internal Cancellable cancellable { get; private set; }
 
     private TransactionType type;
@@ -17,11 +17,11 @@ private class Geary.Db.TransactionAsyncJob : BaseObject {
     private Error? caught_err = null;
 
 
-    public TransactionAsyncJob(Connection? cx,
+    public TransactionAsyncJob(DatabaseConnection? default_cx,
                                TransactionType type,
                                TransactionMethod cb,
                                Cancellable? cancellable) {
-        this.cx = cx;
+        this.default_cx = default_cx;
         this.type = type;
         this.cb = cb;
         this.cancellable = cancellable ?? new Cancellable();
@@ -34,7 +34,7 @@ private class Geary.Db.TransactionAsyncJob : BaseObject {
     }
 
     // Called in background thread context
-    internal void execute(Connection cx) {
+    internal void execute(DatabaseConnection cx) {
         // execute transaction
         try {
             // possible was cancelled during interim of scheduling and execution
diff --git a/src/engine/db/db-transaction-connection.vala b/src/engine/db/db-transaction-connection.vala
new file mode 100644
index 000000000..737828de3
--- /dev/null
+++ b/src/engine/db/db-transaction-connection.vala
@@ -0,0 +1,65 @@
+/*
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2020 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 connection to the database for transactions.
+ */
+internal class Geary.Db.TransactionConnection : Context, Connection {
+
+
+    /** {@inheritDoc} */
+    public Database database { get { return this.db_cx.database; } }
+
+    /** {@inheritDoc} */
+    internal Sqlite.Database db { get { return this.db_cx.db; } }
+
+    internal string[] transaction_log = {};
+
+    private DatabaseConnection db_cx;
+
+
+    internal TransactionConnection(DatabaseConnection db_cx) {
+        this.db_cx = db_cx;
+    }
+
+    /** {@inheritDoc} */
+    public Statement prepare(string sql) throws DatabaseError {
+        this.transaction_log += sql;
+        return this.db_cx.prepare(sql);
+    }
+
+    /** {@inheritDoc} */
+    public Result query(string sql, GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        this.transaction_log += sql;
+        return this.db_cx.query(sql, cancellable);
+    }
+
+    /** {@inheritDoc} */
+    public void exec(string sql, GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        this.transaction_log += sql;
+        this.db_cx.exec(sql, cancellable);
+    }
+
+    /** {@inheritDoc} */
+    public void exec_file(GLib.File file, GLib.Cancellable? cancellable = null)
+        throws GLib.Error {
+        this.transaction_log += file.get_uri();
+        this.db_cx.exec_file(file, cancellable);
+    }
+
+    public override Connection? get_connection() {
+        return this;
+    }
+
+    public string to_string() {
+        return this.db_cx.to_string();
+    }
+
+}
diff --git a/src/engine/db/db-versioned-database.vala b/src/engine/db/db-versioned-database.vala
index ae9717a1b..fc0345e3c 100644
--- a/src/engine/db/db-versioned-database.vala
+++ b/src/engine/db/db-versioned-database.vala
@@ -91,7 +91,7 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
         yield base.open(flags, cancellable);
 
         // get Connection for upgrade activity
-        Connection cx = yield open_connection(cancellable);
+        DatabaseConnection cx = yield open_connection(cancellable);
 
         int db_version = cx.get_user_version_number();
         debug("VersionedDatabase.upgrade: current database schema for %s: %d",
@@ -171,7 +171,7 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
             completed_upgrade(db_version);
     }
 
-    private async void execute_upgrade(Connection cx,
+    private async void execute_upgrade(DatabaseConnection cx,
                                        int db_version,
                                        GLib.File upgrade_script,
                                        Cancellable? cancellable)
diff --git a/src/engine/db/db.vala b/src/engine/db/db.vala
index b062ee9af..f83fd668e 100644
--- a/src/engine/db/db.vala
+++ b/src/engine/db/db.vala
@@ -48,7 +48,10 @@ public enum ResetScope {
 /**
  * See Connection.exec_transaction() for more information on how this delegate is used.
  */
-public delegate TransactionOutcome TransactionMethod(Connection cx, Cancellable? cancellable) throws Error;
+public delegate TransactionOutcome TransactionMethod(
+    Connection cx,
+    GLib.Cancellable? cancellable
+) throws GLib.Error;
 
 // Used by exec_retry_locked().
 private delegate int SqliteExecOperation();
diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala
index 5ede9824c..c7df463b2 100644
--- a/src/engine/imap-db/imap-db-database.vala
+++ b/src/engine/imap-db/imap-db-database.vala
@@ -698,9 +698,11 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
         stmt.exec();
     }
 
-    protected override void prepare_connection(Db.Connection cx)
+    protected override void prepare_connection(Db.DatabaseConnection cx)
         throws GLib.Error {
-        cx.set_busy_timeout_msec(Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC);
+        cx.set_busy_timeout_msec(
+            Db.DatabaseConnection.RECOMMENDED_BUSY_TIMEOUT_MSEC
+        );
         cx.set_foreign_keys(true);
         cx.set_recursive_triggers(true);
         cx.set_synchronous(Db.SynchronousMode.NORMAL);
diff --git a/src/engine/imap-db/imap-db-gc.vala b/src/engine/imap-db/imap-db-gc.vala
index c5773683d..dd090ea8e 100644
--- a/src/engine/imap-db/imap-db-gc.vala
+++ b/src/engine/imap-db/imap-db-gc.vala
@@ -207,7 +207,7 @@ 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);
+        Geary.Db.DatabaseConnection cx = yield db.open_connection(cancellable);
         yield Nonblocking.Concurrent.global.schedule_async(() => {
             cx.exec("VACUUM", cancellable);
 
diff --git a/src/engine/meson.build b/src/engine/meson.build
index 55797de2a..0efd773ea 100644
--- a/src/engine/meson.build
+++ b/src/engine/meson.build
@@ -73,11 +73,13 @@ engine_vala_sources = files(
   'db/db-connection.vala',
   'db/db-context.vala',
   'db/db-database.vala',
+  'db/db-database-connection.vala',
   'db/db-database-error.vala',
   'db/db-result.vala',
   'db/db-statement.vala',
   'db/db-synchronous-mode.vala',
   'db/db-transaction-async-job.vala',
+  'db/db-transaction-connection.vala',
   'db/db-transaction-outcome.vala',
   'db/db-transaction-type.vala',
   'db/db-versioned-database.vala',



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