241 lines
8.7 KiB
Vala
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;
|
|
}
|
|
|
|
}
|