geary/src/engine/db/db-versioned-database.vala
2019-04-02 13:35:25 +11:00

241 lines
8.7 KiB
Vala

/*
* 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 {
private static Geary.Nonblocking.Mutex upgrade_mutex =
new Geary.Nonblocking.Mutex();
public File schema_dir { get; private set; }
/** {@inheritDoc} */
public VersionedDatabase.persistent(File db_file, File schema_dir) {
base.persistent(db_file);
this.schema_dir = schema_dir;
}
/** {@inheritDoc} */
public VersionedDatabase.transient(File schema_dir) {
base.transient();
this.schema_dir = schema_dir;
}
/** Returns the current schema version number of this database. */
public int get_schema_version()
throws GLib.Error {
return get_primary_connection().get_user_version_number();
}
/**
* Called by {@link open} if a schema upgrade is required and beginning.
*
* 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.
*/
protected virtual async void pre_upgrade(int version, Cancellable? cancellable)
throws Error {
}
/**
* Called by {@link open} just after performing a schema upgrade step.
*/
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.
*/
protected virtual void completed_upgrade(int final_version) {
}
/**
* 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.
*/
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 = yield open_connection(cancellable);
int db_version = cx.get_user_version_number();
debug("VersionedDatabase.upgrade: current database schema for %s: %d",
this.path, db_version);
// If the DB doesn't exist yet, the version number will be zero, but also treat negative
// values as new
bool new_db = db_version <= 0;
// Initialize new database to version 1 (note the preincrement in the loop below)
if (db_version < 0)
db_version = 0;
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 (!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>.
int token = yield VersionedDatabase.upgrade_mutex.claim_async(
cancellable
);
Error? locked_err = null;
try {
yield execute_upgrade(
cx, db_version, upgrade_script, cancellable
);
} catch (Error err) {
locked_err = err;
}
VersionedDatabase.upgrade_mutex.release(ref token);
if (locked_err != null) {
throw locked_err;
}
}
if (started)
completed_upgrade(db_version);
}
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);
}
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;
}
}