diff --git a/po/POTFILES.in b/po/POTFILES.in index 404523c7..09c3c970 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 f429b356..4f0859e1 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 * * 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; } + /** The database this connection is associated with. */ + public abstract Database database { get; } - internal Sqlite.Database db; + /** The underlying SQLite database connection. */ + internal abstract Sqlite.Database db { get; } - private int cx_number; - private int busy_timeout_msec = DEFAULT_BUSY_TIMEOUT_MSEC; - - 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; - } - - // 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; - } + public abstract Statement prepare(string sql) + throws DatabaseError; /** - * Starts a new asynchronous transaction for this connection. + * Executes a single SQL statement, returning a result. * - * 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. + * 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. * - * Throws {@link DatabaseError.OPEN_REQUIRED} if not open. + * @see exec */ - 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 - ); + public abstract Result query(string sql, GLib.Cancellable? cancellable = null) + throws GLib.Error; - this.database.add_async_job(job); - return yield job.wait_for_completion_async(); - } + /** + * 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. + * + * There is no way to retrieve a result iterator from this + * call. If needed, use {@link query} instead. + * + * SQLite reference: [[http://www.sqlite.org/c3ref/exec.html]] + */ + 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 00000000..da9f7e7f --- /dev/null +++ b/src/engine/db/db-database-connection.vala @@ -0,0 +1,247 @@ +/* + * Copyright © 2016 Software Freedom Conservancy Inc. + * Copyright © 2020 Michael Gratton + * + * 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 e28180a4..ac201ef0 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? 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 9ab8d6b5..1c242fcd 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 00000000..737828de --- /dev/null +++ b/src/engine/db/db-transaction-connection.vala @@ -0,0 +1,65 @@ +/* + * Copyright © 2016 Software Freedom Conservancy Inc. + * Copyright © 2020 Michael Gratton + * + * 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 ae9717a1..fc0345e3 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 b062ee9a..f83fd668 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 5ede9824..c7df463b 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 c5773683..dd090ea8 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 55797de2..0efd773e 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',