Database threading + WorkerThread, v0



Hi all, 

I've just completed a rather large patch, which rewrites the database
layer almost completely and adds a background thread to do long running
& heavy tasks.

I started this because I was annoyed with the way XMP metadata is
written to files, it blocks the UI. Therefor I added a background
thread, which can be used to do all sorts of background tasks (I'm
thinking of stuff like automatically syncing tags with flickr).

In the process, I had to make the database code thread-aware, as sqlite
doesn't handle threads very well. With the help of Abock, this has been
implemented, largely based on banshee's database code (which has been
known to work ;-)).

Anyway, I'm going to post this in the bugzilla in a couple of days, but
I'd like to ask for some feedback/reviews here first.

Here's how to test it:
 * Start f-spot without the patch, enable XMP metadata writing, add a
tag to 70+ pictures. You'll notice the UI hangs quite a long time (with
loads of console output).
 * Now try the same with the patch applied. Tagging should be a lot
faster. Metadata is written in the background. As a side-effect, f-spot
is now threading-capable, so we can think about doing more things
outside of the GTK mainloop (thus less blocking of the UI).


Any feedback would be greatly appreciated!

Kind regards,
   Ruben 


--
Ruben Vermeersch (rubenv)
http://www.Lambda1.be/
WorkerThread + Database threading.

 ChangeLog           |   21 +++
 src/Core.cs         |    9 +
 src/Db.cs           |  333 +++++++++++++++++++++++++++++++++++++++++-----------
 src/ImportStore.cs  |   53 +-------
 src/MainWindow.cs   |  109 +++++++----------
 src/Makefile.am     |    1 
 src/MetaStore.cs    |   69 ++--------
 src/PhotoStore.cs   |  255 ++++++++++++---------------------------
 src/TagStore.cs     |   54 ++------
 src/Updater.cs      |   40 +-----
 src/WorkerThread.cs |  183 ++++++++++++++++++++++++++++
 11 files changed, 662 insertions(+), 465 deletions(-)


diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/ChangeLog f-spot/ChangeLog
--- f-spot-clean/ChangeLog	2006-03-25 07:51:24.000000000 +0100
+++ f-spot/ChangeLog	2006-04-07 19:12:36.000000000 +0200
@@ -1,3 +1,24 @@
+2006-04-07  Ruben Vermeersch  <ruben Lambda1 be>
+
+	* src/Core.cs: Integrate WorkerThread.
+
+	* src/Db.cs: Rewrite Db to make the whole thing threaded. Based on
+	Aaron Bockover's QueuedSQLiteDatabase used in banshee (with
+	permission). 
+
+	* src/MainWindow.cs: Make Metadata writing a WorkerThread task,
+	accidental formatting cleanup.
+
+	* src/Makefile.am: Add WorkerThread.cs
+
+	* src/ImportStore.cs
+	* src/MetaStore.cs
+	* src/PhotoStore.cs
+	* src/TagStore.cs
+	* src/Updater.cs: Adapt to new Db API.
+
+	* src/WorkerThread.cs: New worker thread system.
+
 2006-03-25  Larry Ewing  <lewing novell com>
 
 	* icons/Makefile.am (noinst_DATA): remove f-spot-find and
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/Core.cs f-spot/src/Core.cs
--- f-spot-clean/src/Core.cs	2006-03-23 21:11:26.000000000 +0100
+++ f-spot/src/Core.cs	2006-04-07 11:56:12.000000000 +0200
@@ -22,9 +22,14 @@ namespace FSpot {
 	{
 		MainWindow organizer;
 		private static Db db;
+		private static WorkerThread worker;
 		static DBus.Connection connection;
 		System.Collections.ArrayList toplevels;
 
+		public static WorkerThread Worker {
+			get { return worker; }
+		}
+
 		public Core ()
 		{
 			toplevels = new System.Collections.ArrayList ();
@@ -106,6 +111,9 @@ namespace FSpot {
 		public override void Organize ()
 		{
 			MainWindow.Window.Present ();
+
+			worker = new WorkerThread(db);
+			GLib.Idle.Add (new GLib.IdleHandler (worker.Install));
 		}
 		
 		public override void View (string list)
@@ -227,6 +235,7 @@ namespace FSpot {
 
 		public override void Shutdown ()
 		{
+			worker.Shutdown();
 			System.Environment.Exit (0);
 		}
 
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/Db.cs f-spot/src/Db.cs
--- f-spot-clean/src/Db.cs	2006-03-09 06:20:46.000000000 +0100
+++ f-spot/src/Db.cs	2006-04-07 18:40:39.000000000 +0200
@@ -1,5 +1,6 @@
 using Mono.Data.SqliteClient;
 using System.Collections;
+using System.Threading;
 using System.IO;
 using System;
 
@@ -126,20 +127,20 @@ public abstract class DbStore {
 
 	// Sqlite stuff.
 
-	SqliteConnection connection;
-	protected SqliteConnection Connection {
+	Db database;
+	protected Db Database {
 		get {
-			return connection;
+			return database;
 		}
 	}
 
 
 	// Constructor.
 
-	public DbStore (SqliteConnection connection,
+	public DbStore (Db database,
 			bool cache_is_immortal)
 	{
-		this.connection = connection;
+		this.database = database;
 		this.cache_is_immortal = cache_is_immortal;
 
 		item_cache = new Hashtable ();
@@ -163,27 +164,15 @@ public abstract class DbStore {
 	}
 
 	public void BeginTransaction () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = "BEGIN TRANSACTION";
-		command.ExecuteScalar ();
-		command.Dispose ();
+		Database.BeginTransaction ();
 	}
 	
 	public void CommitTransaction () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = "COMMIT TRANSACTION";
-		command.ExecuteScalar ();
-		command.Dispose ();
+		Database.CommitTransaction ();
 	}
 	
 	public void RollbackTransaction () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = "ROLLBACK";
-		command.ExecuteScalar ();
-		command.Dispose ();
+		Database.RollbackTransaction ();
 	}
 
 }
@@ -191,13 +180,23 @@ public abstract class DbStore {
 
 // The Database puts the stores together.
 
-public class Db : IDisposable {
+public class Db : IDisposable
+{
+	private ArrayList command_queue = new ArrayList();
+	private ArrayList non_transaction_command_queue = new ArrayList();
+	private SqliteConnection connection;
+	private Thread queue_thread;
+	private bool dispose_requested = false;
+	private bool processing_queue = false;
+	private bool empty;
+
+	private Thread current_transaction = null;
 
 	TagStore tag_store;
 	PhotoStore photo_store;
  	ImportStore import_store;
  	MetaStore meta_store;
-	bool empty;
+	WorkerThreadStore jobs_store;
 
 	public TagStore Tags {
 		get { return tag_store; }
@@ -215,45 +214,80 @@ public class Db : IDisposable {
 		get { return meta_store; }
 	}
 
-	// This affects how often the database writes data back to disk, and
-	// therefore how likely corruption is in the event of power loss.
-	public bool Sync {
-		set {
-			SqliteCommand command = new SqliteCommand ();
-			command.Connection = sqlite_connection;
-			command.CommandText = "PRAGMA synchronous = " + (value ? "ON" : "OFF");
-			command.ExecuteScalar ();
-			command.Dispose ();
-		}
+	public WorkerThreadStore Jobs {
+		get { return jobs_store; }
 	}
 
-	public void BeginTransaction () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = sqlite_connection;
-		command.CommandText = "BEGIN TRANSACTION";
-		command.ExecuteScalar ();
-		command.Dispose ();
-	}
-	
-	public void CommitTransaction () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = sqlite_connection;
-		command.CommandText = "COMMIT TRANSACTION";
-		command.ExecuteScalar ();
-		command.Dispose ();
+	public void Dispose()
+	{
+		dispose_requested = true;
+		while(processing_queue);
 	}
-	
-	public void RollbackTransaction () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = sqlite_connection;
-		command.CommandText = "ROLLBACK";
-		command.ExecuteScalar ();
-		command.Dispose ();
+
+	private void QueueCommand(QueuedSqliteCommand command)
+	{
+		if (current_transaction != null && current_transaction != Thread.CurrentThread) {
+			lock(non_transaction_command_queue.SyncRoot) {
+				non_transaction_command_queue.Add(command);
+			}
+		} else {
+			lock(command_queue.SyncRoot) {
+				command_queue.Add(command);
+			}
+		}
 	}
 
-	SqliteConnection sqlite_connection;
-	public SqliteConnection Connection {
-		get { return sqlite_connection; }
+	/*
+	 * Performs query and returns the selected data (use for SELECT)
+	 */
+	public SqliteDataReader Query(object query)
+	{
+		QueuedSqliteCommand command = new QueuedSqliteCommand(connection, 
+		query.ToString(), DbCommandType.Reader);
+		QueueCommand(command);
+		return command.WaitForResult() as SqliteDataReader;
+	}
+		
+	/*
+	 * Performs query and returns a single result
+	 */
+	public object QuerySingle(object query)
+	{
+		QueuedSqliteCommand command = new QueuedSqliteCommand(connection, 
+		query.ToString(), DbCommandType.Scalar);
+		QueueCommand(command);
+		return command.WaitForResult(); 
+	}
+
+	/*
+	 * Performs query and returns the insert ID (if any)
+	 */
+	public uint Execute(object query)
+	{
+		QueuedSqliteCommand command = new QueuedSqliteCommand(connection, 
+		query.ToString(), DbCommandType.Execute);
+		QueueCommand(command);
+		command.WaitForResult();
+		return command.InsertID;
+	}
+
+	/*
+	 * Performs a query and doesn't wait for a result
+	 */
+	public void ExecuteNoWait(object query)
+	{
+		QueuedSqliteCommand command = new QueuedSqliteCommand(connection, 
+		query.ToString(), DbCommandType.ExecuteNoWait);
+		QueueCommand(command);
+	}
+
+	public bool TableExists(string table)
+	{
+		return Convert.ToInt32(QuerySingle(String.Format(@"
+		SELECT COUNT(*) 
+			FROM sqlite_master
+			WHERE Type='table' AND Name='{0}'", 
+			table))) > 0;
 	}
 
 	private static int GetFileVersion (string path)
@@ -284,7 +318,7 @@ public class Db : IDisposable {
 			throw new Exception (path + ": File not found");
 
 		if (! new_db) {
-			int version = Db.GetFileVersion (path);
+			int version = GetFileVersion (path);
 			// FIXME: we should probably display and error dialog if the version
 			// is anything other than the one we were built with, but for now at least
 			// use the right version.
@@ -298,20 +332,25 @@ public class Db : IDisposable {
 				version_string += ",encoding=UTF-8";
 		}
 		
-		sqlite_connection = new SqliteConnection ();
-		sqlite_connection.ConnectionString = "URI=file:" + path + version_string;
+		connection = new SqliteConnection ();
+		connection.ConnectionString = "URI=file:" + path + version_string;
 
-		sqlite_connection.Open ();
+		connection.Open ();
+
+		queue_thread = new Thread(ProcessQueue);
+		queue_thread.IsBackground = true;
+		queue_thread.Start();
 		
 		// Load or create the meta table
- 		meta_store = new MetaStore (sqlite_connection, new_db);
+ 		meta_store = new MetaStore (this, new_db);
 
 		// Update the database schema if necessary
 		FSpot.Database.Updater.Run (this);
 
-		tag_store = new TagStore (sqlite_connection, new_db);
-		import_store = new ImportStore (sqlite_connection, new_db);
- 		photo_store = new PhotoStore (sqlite_connection, new_db, tag_store);
+		tag_store = new TagStore (this, new_db);
+		import_store = new ImportStore (this, new_db);
+ 		photo_store = new PhotoStore (this, new_db, tag_store);
+		jobs_store = new WorkerThreadStore (this, new_db);
 		
 		empty = new_db;
 	}
@@ -322,9 +361,173 @@ public class Db : IDisposable {
 		}
 	}
 
-	public void Dispose () {}
+	private void ProcessQueue()
+	{		 
+		processing_queue = true;
+		bool in_dispose_transaction = false;
+
+		int sleep_time = 10;
+		bool queries = false;
+		
+		while(true) {
+			queries = false;
+			while(command_queue.Count > 0) {
+				queries = true;
+				if(dispose_requested && current_transaction == null && !in_dispose_transaction) {
+					(new SqliteCommand("BEGIN", connection)).ExecuteNonQuery();
+					in_dispose_transaction = true;
+				}
+				
+				QueuedSqliteCommand command = command_queue[0] as QueuedSqliteCommand;
+				command.Execute();
+				
+				// TODO: optimize (RemoveAt?)
+				lock(command_queue.SyncRoot) {
+					command_queue.Remove(command);
+				}
+			}
+
+			if(dispose_requested && current_transaction == null) {
+				if(in_dispose_transaction) {
+					(new SqliteCommand("COMMIT", connection)).ExecuteNonQuery();
+				}
+				connection.Close();
+				processing_queue = false;
+				return;
+			}
+
+			if (!queries && sleep_time < 20) {
+				sleep_time++;
+			} else if (queries && sleep_time > 1) {
+				sleep_time--;
+			}
+			
+			Thread.Sleep(sleep_time);
+		}
+	}
+	
+	// This affects how often the database writes data back to disk, and
+	// therefore how likely corruption is in the event of power loss.
+	public bool Sync {
+		set {
+			Execute("PRAGMA synchronous = " + (value ? "ON" : "OFF"));
+		}
+	}
+
+	public void BeginTransaction() {
+		while (current_transaction != null);
+		current_transaction = Thread.CurrentThread;
+		Execute("BEGIN TRANSACTION");
+	}
+
+	public void CommitTransaction () {
+		if (current_transaction != Thread.CurrentThread) {
+			throw new ApplicationException("Can't commit when not in a transaction!");
+		}
+		Execute("COMMIT TRANSACTION");
+		current_transaction = null;
+		PushNonTransactionQueue();
+	}
+
+	public void RollbackTransaction () {
+		if (current_transaction != Thread.CurrentThread) {
+			throw new ApplicationException("Can't rollback when not in a transaction!");
+		}
+		Execute("ROLLBACK");
+		current_transaction = null;
+		PushNonTransactionQueue();
+	}
+
+	// Pull out everything that was sent in during the transaction
+	private void PushNonTransactionQueue() {
+		while (non_transaction_command_queue.Count > 0) {
+			// FIXME: better check we are really really sure this can never
+			//        cause deadlocks!
+			lock(command_queue.SyncRoot) {
+			lock (non_transaction_command_queue.SyncRoot) {
+				QueuedSqliteCommand command = non_transaction_command_queue[0] as QueuedSqliteCommand;
+				command_queue.Add(command);
+				non_transaction_command_queue.Remove(command);
+			}
+			}
+		}
+	}
+}
+
+internal enum DbCommandType {
+	Reader,
+	Scalar,
+	Execute,
+	ExecuteNoWait
 }
 
+internal class QueuedSqliteCommand : SqliteCommand
+{
+	private DbCommandType command_type;
+	private object result;
+	private uint insert_id;
+	private Exception execution_exception;
+	private bool finished = false;
+
+	public QueuedSqliteCommand(SqliteConnection connection, string command, DbCommandType commandType)
+		: base(command, connection)
+	{
+		this.command_type = commandType;
+	}
+
+	public void Execute()
+	{
+		if(result != null) {
+			throw new ApplicationException("Command has alread been executed");
+		}
+
+		try {
+			switch(command_type) {
+				case DbCommandType.Reader:
+					result = ExecuteReader();
+					break;
+				case DbCommandType.Scalar:
+					result = ExecuteScalar();
+					break;
+				case DbCommandType.Execute:
+				case DbCommandType.ExecuteNoWait:
+				default:
+					result = ExecuteNonQuery();
+					insert_id = (uint) LastInsertRowID();
+					break;
+			}
+		} catch(Exception e) {
+			execution_exception = e;
+
+			// Throw exception anyway, else it will go by unnoticed.
+			if (command_type == DbCommandType.ExecuteNoWait) {
+				throw e;
+			}
+		}
+		
+		finished = true;
+	}
+
+	public object WaitForResult()
+	{
+		while(!finished);
+		
+		if(execution_exception != null) {
+			throw execution_exception;
+		}
+		
+		return result;
+	}
+
+	public object Result {
+		get { return result; }
+		internal set { result = value; }
+	}
+
+	public uint InsertID {
+		get { return insert_id; }
+	}
+}
 
 public class DbUtils {
 	public static DateTime DateTimeFromUnixTime (long unix_time)
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/ImportStore.cs f-spot/src/ImportStore.cs
--- f-spot-clean/src/ImportStore.cs	2006-01-22 23:43:53.000000000 +0100
+++ f-spot/src/ImportStore.cs	2006-04-07 18:41:39.000000000 +0200
@@ -30,38 +30,25 @@ public class ImportStore : DbStore {
 
 	// Constructor
 
-	public ImportStore (SqliteConnection connection, bool is_new)
-		: base (connection, false)
+	public ImportStore (Db database, bool is_new)
+		: base (database, false)
 	{
-		if (! is_new)
+		bool exists = database.TableExists("imports");
+		if (!is_new || exists)
 			return;
 		
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText =
+		Database.QuerySingle(
 			"CREATE TABLE imports (                            " +
 			"	id          INTEGER PRIMARY KEY NOT NULL,  " +
 			"       time        INTEGER			   " +
-			")";
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+			")");
 	}
 
 	public Import Create (DateTime time_in_utc)
 	{
 		long unix_time = DbUtils.UnixTimeFromDateTime (time_in_utc);
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("INSERT INTO import (time) VALUES ({0})  ",
-						     unix_time);
-		command.ExecuteScalar ();
-		command.Dispose ();
-
-		uint id = (uint) Connection.LastInsertRowId;
+		uint id = Database.Execute(String.Format ("INSERT INTO import (time) VALUES ({0})  ", unix_time));
 		Import import = new Import (id, unix_time);
 		AddToCache (import);
 
@@ -74,19 +61,14 @@ public class ImportStore : DbStore {
 		if (import != null)
 			return import;
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("SELECT time FROM imports WHERE id = {0}", id);
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = String.Format ("SELECT time FROM imports WHERE id = {0}", id);
+		SqliteDataReader reader = Database.Query(query);
 
 		if (reader.Read ()) {
 			import = new Import (id, Convert.ToUInt32 (reader [0]));
 			AddToCache (import);
 		}
 
-		command.Dispose ();
-
 		return import;
 	}
 
@@ -94,13 +76,8 @@ public class ImportStore : DbStore {
 	{
 		RemoveFromCache (item);
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("DELETE FROM imports WHERE id = {0}", item.Id);
-		command.ExecuteNonQuery ();
-
-		command.Dispose ();
+		string query = String.Format ("DELETE FROM imports WHERE id = {0}", item.Id);
+		Database.ExecuteNoWait (query);
 	}
 
 	public override void Commit (DbItem item)
@@ -113,11 +90,7 @@ public class ImportStore : DbStore {
 	{
 		ArrayList list = new ArrayList ();
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = "SELECT id, time FROM imports";
-		SqliteDataReader reader = command.ExecuteReader ();
+		SqliteDataReader reader = Database.Query ("SELECT id, time FROM imports");
 
 		while (reader.Read ()) {
 			// Note that we get both time and ID from the database, but we have to see
@@ -135,8 +108,6 @@ public class ImportStore : DbStore {
 			list.Add (import);
 		}
 
-		command.Dispose ();
-
 		list.Sort (new ImportComparerByDate ());
 		return list;
 	}
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/MainWindow.cs f-spot/src/MainWindow.cs
--- f-spot-clean/src/MainWindow.cs	2006-03-23 01:39:28.000000000 +0100
+++ f-spot/src/MainWindow.cs	2006-04-07 18:08:13.000000000 +0200
@@ -18,7 +18,7 @@ using LibGPhoto2;
 
 public class MainWindow {
 
-        public static MainWindow Toplevel;
+	public static MainWindow Toplevel;
 
 	Db db;
 
@@ -442,15 +442,8 @@ public class MainWindow {
 
 	private void HandleDbItemsChanged (object sender, DbItemEventArgs args)
 	{
-		foreach (DbItem item in args.Items) {
-			Photo p = item as Photo;
-			if (p == null)
-				continue;
-			
-			if (write_metadata)
-				p.WriteMetadataToImage ();
-			
-		}
+		if (write_metadata)
+			Core.Worker.ScheduleMetaDataWrites(args.Items);
 		
 		if (args is TimeChangedEventArgs)
 			query.RequestReload ();
@@ -2638,59 +2631,59 @@ public class MainWindow {
                        return;
                }
 
-	       // Add any new tags to the selected photos
-	       Category default_category = null;
-	       Tag [] selection = tag_selection_widget.TagHighlight;
-	       if (selection.Length > 0) {
-		       if (selection [0] is Category)
-			       default_category = (Category) selection [0];
-		       else
-			       default_category = selection [0].Category;
-	       }
+		// Add any new tags to the selected photos
+		Category default_category = null;
+		Tag [] selection = tag_selection_widget.TagHighlight;
+		if (selection.Length > 0) {
+			if (selection [0] is Category)
+				default_category = (Category) selection [0];
+			else
+				default_category = selection [0].Category;
+		}
 
 		db.BeginTransaction ();
-	       for (int i = 0; i < tagnames.Length; i ++) {
-		       if (tagnames [i].Length == 0)
-			       continue;
-
-		       Tag t = db.Tags.GetTagByName (tagnames [i]);
-
-		       if (t == null) {
-			       t = db.Tags.CreateCategory (default_category, tagnames [i]) as Tag;
-			       db.Tags.Commit (t);
-		       }
-
-		       // Correct for capitalization differences
-		       tagnames [i] = t.Name;
-
-		       Tag [] tags = new Tag [1];
-		       tags [0] = t;
-
-		       foreach (int num in selected_photos)
-			       AddTagExtended (num, tags);
-	       }
+		for (int i = 0; i < tagnames.Length; i ++) {
+			if (tagnames [i].Length == 0)
+				continue;
+
+			Tag t = db.Tags.GetTagByName (tagnames [i]);
+
+			if (t == null) {
+				t = db.Tags.CreateCategory (default_category, tagnames [i]) as Tag;
+				db.Tags.Commit (t);
+			}
+
+			// Correct for capitalization differences
+			tagnames [i] = t.Name;
+
+			Tag [] tags = new Tag [1];
+			tags [0] = t;
+
+			foreach (int num in selected_photos)
+				AddTagExtended (num, tags);
+		}
 		db.CommitTransaction ();
-	       
-	       // Remove any removed tags from the selected photos
-	       foreach (string tagname in selected_photos_tagnames) {
-		       if (! IsTagInList (tagnames, tagname)) {
+		
+		// Remove any removed tags from the selected photos
+		foreach (string tagname in selected_photos_tagnames) {
+			if (! IsTagInList (tagnames, tagname)) {
 				
-			       Tag tag = db.Tags.GetTagByName (tagname);
+				Tag tag = db.Tags.GetTagByName (tagname);
 
-			       foreach (int num in selected_photos) {
-				       query.Photos [num].RemoveTag (tag);
-				       query.Commit (num);
-			       }
-		       }
-	       }
-
-	       if (view_mode == ModeType.IconView) {
-		       icon_view.QueueDraw ();
-		       icon_view.GrabFocus ();
-	       } else {
-		       photo_view.QueueDraw ();
-		       photo_view.View.GrabFocus ();
-	       }
+				foreach (int num in selected_photos) {
+					query.Photos [num].RemoveTag (tag);
+					query.Commit (num);
+				}
+			}
+		}
+
+		if (view_mode == ModeType.IconView) {
+			icon_view.QueueDraw ();
+			icon_view.GrabFocus ();
+		} else {
+			photo_view.QueueDraw ();
+			photo_view.View.GrabFocus ();
+		}
 	}
 
 	private void HideTagbar ()
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/Makefile.am f-spot/src/Makefile.am
--- f-spot-clean/src/Makefile.am	2006-02-26 23:37:08.000000000 +0100
+++ f-spot/src/Makefile.am	2006-04-06 18:34:47.000000000 +0200
@@ -115,6 +115,7 @@ F_SPOT_CSDISTFILES =				\
 	$(srcdir)/CameraSelectionDialog.cs	\
 	$(srcdir)/CameraFileSelectionDialog.cs	\
 	$(srcdir)/TagSelectionDialog.cs		\
+	$(srcdir)/WorkerThread.cs			\
 	$(srcdir)/X3fFile.cs			\
 	$(srcdir)/XmpFile.cs			\
 	$(srcdir)/main.cs
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/MetaStore.cs f-spot/src/MetaStore.cs
--- f-spot-clean/src/MetaStore.cs	2006-03-02 04:17:23.000000000 +0100
+++ f-spot/src/MetaStore.cs	2006-04-07 17:10:40.000000000 +0200
@@ -60,18 +60,12 @@ public class MetaStore : DbStore {
 
 	private void CreateTable ()
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText =
+		Database.Execute(
 			@"CREATE TABLE meta (
 				id		INTEGER PRIMARY KEY NOT NULL,
 				name		TEXT UNIQUE NOT NULL,
 				data		TEXT
-			)";
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+			)");
 	}
 
 	private void CreateDefaultItems (bool is_new)
@@ -80,29 +74,19 @@ public class MetaStore : DbStore {
 		Create (db_version, (is_new) ? FSpot.Database.Updater.LatestVersion.ToString () : "0");
 		
 		// Get the hidden tag id, if it exists
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = "SELECT id FROM tags WHERE name = 'Hidden'";
-		
 		try {
-			SqliteDataReader reader = command.ExecuteReader ();
+			SqliteDataReader reader = Database.Query("SELECT id FROM tags WHERE name = 'Hidden'");
 		
 			if (reader.Read ())
 				Create (hidden, reader [0].ToString ());
 
 			reader.Close ();
 		} catch (Exception e) {}
-	
-		command.Dispose ();
 	}
 	
 	private void LoadAllItems ()
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = "SELECT id, name, data FROM meta";
-		SqliteDataReader reader = command.ExecuteReader ();
+		SqliteDataReader reader = Database.Query("SELECT id, name, data FROM meta");
 
 		while (reader.Read ()) {
 			uint id = Convert.ToUInt32 (reader [0]);
@@ -119,7 +103,6 @@ public class MetaStore : DbStore {
 		}
 
 		reader.Close ();
-		command.Dispose ();
 
 		if (FSpotVersion.Value != FSpot.Defines.VERSION) {
 			FSpotVersion.Value = FSpot.Defines.VERSION;
@@ -129,16 +112,11 @@ public class MetaStore : DbStore {
 
 	private MetaItem Create (string name, string data)
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("INSERT INTO meta (name, data) VALUES ('{0}', {1})",
+		string query = String.Format ("INSERT INTO meta (name, data) VALUES ('{0}', {1})",
 				name, (data == null) ? "NULL" : "'" + data + "'");
 		
-		MetaItem item = new MetaItem ((uint) Connection.LastInsertRowId, name, data);
-
-		command.ExecuteScalar ();
-		command.Dispose ();
+		uint insert_id = Database.Execute(query);
+		MetaItem item = new MetaItem (insert_id, name, data);
 		
 		AddToCache (item);
 		EmitAdded (item);
@@ -150,13 +128,8 @@ public class MetaStore : DbStore {
 	{
 		MetaItem item = dbitem as MetaItem;
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("UPDATE meta SET data = '{1}' WHERE name = '{0}'", item.Name, item.Value);
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+		string query = String.Format ("UPDATE meta SET data = '{1}' WHERE name = '{0}'", item.Name, item.Value);
+		Database.Execute(query);
 		
 		EmitChanged (item);
 	}
@@ -170,33 +143,19 @@ public class MetaStore : DbStore {
 	{
 		RemoveFromCache (item);
 		
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
+		string query = String.Format ("DELETE FROM meta WHERE id = {0}", item.Id);
+		Database.Execute(query);
 
-		command.CommandText = String.Format ("DELETE FROM meta WHERE id = {0}", item.Id);
-		command.ExecuteNonQuery ();
-
-		command.Dispose ();
 		EmitRemoved (item);
 	}
 
 	// Constructor
 
-	public MetaStore (SqliteConnection connection, bool is_new)
-		: base (connection, true)
+	public MetaStore (Db database, bool is_new)
+		: base (database, true)
 	{
 		// Ensure the table exists
-		bool exists = true;
-		try {
-			SqliteCommand command = new SqliteCommand ();
-			command.Connection = connection;
-			command.CommandText = "UPDATE meta SET id = 1 WHERE 1 = 2";
-			command.ExecuteScalar ();
-			command.Dispose ();
-		} catch (Exception e) {
-			// Table doesn't exist, so create it
-			exists = false;
-		}
+		bool exists = Database.TableExists("meta");
 			
 		if (is_new || !exists) {
 			CreateTable ();
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/PhotoStore.cs f-spot/src/PhotoStore.cs
--- f-spot-clean/src/PhotoStore.cs	2006-03-23 04:17:30.000000000 +0100
+++ f-spot/src/PhotoStore.cs	2006-04-07 18:13:31.000000000 +0200
@@ -644,19 +644,16 @@ public class PhotoStore : DbStore {
 
 	// Constructor
 
-	public PhotoStore (SqliteConnection connection, bool is_new, TagStore tag_store)
-		: base (connection, false)
+	public PhotoStore (Db database, bool is_new, TagStore tag_store)
+		: base (database, false)
 	{
 		this.tag_store = tag_store;
 		EnsureThumbnailDirectory ();
 
 		if (! is_new)
 			return;
-		
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
 
-		command.CommandText =
+		Database.Execute(
 			"CREATE TABLE photos (                                     " +
 			"	id                 INTEGER PRIMARY KEY NOT NULL,   " +
 			"       time               INTEGER NOT NULL,	   	   " +
@@ -664,39 +661,20 @@ public class PhotoStore : DbStore {
 			"       name               STRING NOT NULL,		   " +
 			"       description        TEXT NOT NULL,	           " +
 			"       default_version_id INTEGER NOT NULL		   " +
-			")";
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
-
-		// FIXME: No need to do Dispose here?
-
-		command = new SqliteCommand ();
-		command.Connection = Connection;
+			")");
 
-		command.CommandText =
+		Database.Execute(
 			"CREATE TABLE photo_tags (     " +
 			"	photo_id      INTEGER, " +
 			"       tag_id        INTEGER  " +
-			")";
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
-
-		// FIXME: No need to do Dispose here?
-
-		command = new SqliteCommand ();
-		command.Connection = Connection;
+			")");
 
-		command.CommandText =
+		Database.Execute(
 			"CREATE TABLE photo_versions (    " +
 			"       photo_id        INTEGER,  " +
 			"       version_id      INTEGER,  " +
 			"       name            STRING    " +
-			")";
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+			")");
 	}
 
 
@@ -710,22 +688,17 @@ public class PhotoStore : DbStore {
 		FSpot.ImageFile img = FSpot.ImageFile.Create (origPath);
 		long unix_time = DbUtils.UnixTimeFromDateTime (img.Date);
 		string description = img.Description != null  ? img.Description.Split ('\0') [0] : "";
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("INSERT INTO photos (time, " +
-						     "directory_path, name, description, default_version_id) " +
-						     "       VALUES ({0}, '{1}', '{2}', '{3}', {4})",
-						     unix_time,
-						     SqlString (System.IO.Path.GetDirectoryName (newPath)),
-						     SqlString (System.IO.Path.GetFileName (newPath)),
-						     SqlString (description),
-						     Photo.OriginalVersionId);
 
-		command.ExecuteScalar ();
-		command.Dispose ();
+		string query = String.Format ("INSERT INTO photos (time, " +
+					      "directory_path, name, description, default_version_id) " +
+					      "       VALUES ({0}, '{1}', '{2}', '{3}', {4})",
+					      unix_time,
+					      SqlString (System.IO.Path.GetDirectoryName (newPath)),
+					      SqlString (System.IO.Path.GetFileName (newPath)),
+					      SqlString (description),
+					      Photo.OriginalVersionId);
+		uint id = Database.Execute(query);
 
-		uint id = (uint) Connection.LastInsertRowId;
 		Photo photo = new Photo (id, unix_time, newPath);
 		AddToCache (photo);
 		photo.Loaded = true;
@@ -739,43 +712,30 @@ public class PhotoStore : DbStore {
 	private void GetVersions (Photo photo)
 	{
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT version_id, name FROM photo_versions WHERE photo_id = {0}", photo.Id);
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = String.Format ("SELECT version_id, name FROM photo_versions WHERE photo_id = {0}", photo.Id);
+		SqliteDataReader reader = Database.Query(query);
 
 		while (reader.Read ()) {
 			uint version_id = Convert.ToUInt32 (reader [0]);
 			string name = reader[1].ToString ();
 			photo.AddVersionUnsafely (version_id, name);
 		}
-
-		command.Dispose ();
 	}
 
 	private void GetTags (Photo photo)
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT tag_id FROM photo_tags WHERE photo_id = {0}", photo.Id);
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = String.Format ("SELECT tag_id FROM photo_tags WHERE photo_id = {0}", photo.Id);
+		SqliteDataReader reader = Database.Query(query); 
 
 		while (reader.Read ()) {
 			uint tag_id = Convert.ToUInt32 (reader [0]);
 			Tag tag = tag_store.Get (tag_id) as Tag;
 			photo.AddTagUnsafely (tag);
 		}
-
-		command.Dispose ();
 	}		
 	
 	private void GetAllVersions  () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT photo_id, version_id, name " +
-						     "FROM photo_versions ");
-		
-		SqliteDataReader reader = command.ExecuteReader ();
+		SqliteDataReader reader = Database.Query("SELECT photo_id, version_id, name FROM photo_versions");
 		
 		while (reader.Read ()) {
 			uint id = Convert.ToUInt32 (reader [0]);
@@ -810,12 +770,7 @@ public class PhotoStore : DbStore {
 	}
 
 	private void GetAllTags () {
-			SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT photo_id, tag_id " +
-						     "FROM photo_tags ");
-		
-		SqliteDataReader reader = command.ExecuteReader ();
+		SqliteDataReader reader = Database.Query("SELECT photo_id, tag_id FROM photo_tags");
 
 		while (reader.Read ()) {
 			uint id = Convert.ToUInt32 (reader [0]);
@@ -840,13 +795,9 @@ public class PhotoStore : DbStore {
 	}
 
 	private void GetAllData () {
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT photo_tags.photo_id, tag_id, version_id, name " +
-						     "FROM photo_tags, photo_versions " +
-						     "WHERE photo_tags.photo_id = photo_versions.photo_id");
-		
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = "SELECT photo_tags.photo_id, tag_id, version_id, name " +
+			       "FROM photo_tags, photo_versions WHERE photo_tags.photo_id = photo_versions.photo_id";
+		SqliteDataReader reader = Database.Query(query);
 
 		while (reader.Read ()) {
 			uint id = Convert.ToUInt32 (reader [0]);
@@ -878,14 +829,11 @@ public class PhotoStore : DbStore {
 
 	private void GetData (Photo photo)
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT tag_id, version_id, name " +
-						     "  FROM photo_tags, photo_versions " +
-						     " WHERE photo_tags.photo_id = photo_versions.photo_id " +
-						     "   AND photo_tags.photo_id = {0}", photo.Id);
-
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = String.Format ("SELECT tag_id, version_id, name " +
+					      "  FROM photo_tags, photo_versions " +
+					      " WHERE photo_tags.photo_id = photo_versions.photo_id " +
+					      "   AND photo_tags.photo_id = {0}", photo.Id);
+		SqliteDataReader reader = Database.Query(query);
 
 		while (reader.Read ()) {
 		        if (reader [0] != null) {
@@ -908,17 +856,15 @@ public class PhotoStore : DbStore {
 		if (photo != null)
 			return photo;
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("SELECT time,                                 " +
-						     "       directory_path,                       " +
-						     "       name,                                 " +
-						     "       description,                          " +
-						     "       default_version_id                    " +
-						     "     FROM photos                             " +
-						     "     WHERE id = {0}                          ",
-						     id);
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = String.Format ("SELECT time,                                 " +
+					      "       directory_path,                       " +
+					      "       name,                                 " +
+					      "       description,                          " +
+					      "       default_version_id                    " +
+					      "     FROM photos                             " +
+					      "     WHERE id = {0}                          ",
+					      id);
+		SqliteDataReader reader = Database.Query(query);
 
 		if (reader.Read ()) {
 			photo = new Photo (id,
@@ -931,8 +877,6 @@ public class PhotoStore : DbStore {
 			AddToCache (photo);
 		}
 
-		command.Dispose ();
-
 		if (photo == null)
 			return null;
 		
@@ -948,23 +892,20 @@ public class PhotoStore : DbStore {
 		//        this is only used for DND
 
 		Photo photo = null;
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
 
 		string directory_path = System.IO.Path.GetDirectoryName (path);
 		string filename = System.IO.Path.GetFileName (path);
 
-		command.CommandText = String.Format ("SELECT id,                                   " +
-				                     "       time,                                 " +
-						     "       description,                          " +
-						     "       default_version_id                    " +
-						     "  FROM photos                                " +
-						     " WHERE directory_path = \"{0}\"              " +
-						     "   AND name = \"{1}\"                        ",
-						     directory_path,
-						     filename);
-
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = String.Format ("SELECT id,                                   " +
+					      "       time,                                 " +
+					      "       description,                          " +
+					      "       default_version_id                    " +
+					      "  FROM photos                                " +
+					      " WHERE directory_path = \"{0}\"              " +
+					      "   AND name = \"{1}\"                        ",
+					      directory_path,
+					      filename);
+		SqliteDataReader reader = Database.Query(query);
 
 		if (reader.Read ()) {
 			photo = new Photo (Convert.ToUInt32 (reader [0]),
@@ -977,8 +918,6 @@ public class PhotoStore : DbStore {
 			AddToCache (photo);
 		}
 
-		command.Dispose ();
-
 		if (photo == null)
 			return null;
 		
@@ -1019,29 +958,14 @@ public class PhotoStore : DbStore {
 			RemoveFromCache (items[i]);
 		}
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("DELETE FROM photos WHERE {0}", query_builder.ToString ());
-		command.ExecuteNonQuery ();
-
-		command.Dispose ();
-
-		command = new SqliteCommand ();
-		command.Connection = Connection;
+		string query = String.Format ("DELETE FROM photos WHERE {0}", query_builder.ToString ());
+		Database.ExecuteNoWait(query);
 
-		command.CommandText = String.Format ("DELETE FROM photo_tags WHERE {0}", tv_query_builder.ToString ());
-		command.ExecuteNonQuery ();
+		query = String.Format ("DELETE FROM photo_tags WHERE {0}", tv_query_builder.ToString ());
+		Database.ExecuteNoWait(query);
 
-		command.Dispose ();
-
-		command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("DELETE FROM photo_versions WHERE {0}", tv_query_builder.ToString ());
-		command.ExecuteNonQuery ();
-
-		command.Dispose ();
+		query = String.Format ("DELETE FROM photo_versions WHERE {0}", tv_query_builder.ToString ());
+		Database.ExecuteNoWait(query);
 	}
 
 	public override void Remove (DbItem item)
@@ -1071,45 +995,30 @@ public class PhotoStore : DbStore {
 	
         private void Update (Photo photo) {
 		// Update photo.
-
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("UPDATE photos SET description = '{0}',     " +
-						     "                  default_version_id = {1}, " +
-						     "                  time = {2} " +
-						     "              WHERE id = {3}",
-						     SqlString (photo.Description),
-						     photo.DefaultVersionId,
-						     DbUtils.UnixTimeFromDateTime (photo.Time),
-						     photo.Id);
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+		string query = String.Format ("UPDATE photos SET description = '{0}',     " +
+					      "                  default_version_id = {1}, " +
+					      "                  time = {2} " +
+					      "              WHERE id = {3}",
+					      SqlString (photo.Description),
+					      photo.DefaultVersionId,
+					      DbUtils.UnixTimeFromDateTime (photo.Time),
+					      photo.Id);
+		Database.ExecuteNoWait(query);
 
 		// Update tags.
-
-		command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("DELETE FROM photo_tags WHERE photo_id = {0}", photo.Id);
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+		query = String.Format ("DELETE FROM photo_tags WHERE photo_id = {0}", photo.Id);
+		Database.Execute(query);
 
 		foreach (Tag tag in photo.Tags) {
-			command = new SqliteCommand ();
-			command.Connection = Connection;
-			command.CommandText = String.Format ("INSERT INTO photo_tags (photo_id, tag_id) " +
-							     "       VALUES ({0}, {1})",
-							     photo.Id, tag.Id);
-			command.ExecuteNonQuery ();
-			command.Dispose ();
+			query = String.Format ("INSERT INTO photo_tags (photo_id, tag_id) " +
+					       "       VALUES ({0}, {1})",
+					       photo.Id, tag.Id);
+			Database.ExecuteNoWait(query);
 		}
 
 		// Update versions.
-
-		command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = String.Format ("DELETE FROM photo_versions WHERE photo_id = {0}", photo.Id);
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+		query = String.Format ("DELETE FROM photo_versions WHERE photo_id = {0}", photo.Id);
+		Database.Execute(query);
 
 		foreach (uint version_id in photo.VersionIds) {
 			if (version_id == Photo.OriginalVersionId)
@@ -1117,13 +1026,10 @@ public class PhotoStore : DbStore {
 
 			string version_name = photo.GetVersionName (version_id);
 
-			command = new SqliteCommand ();
-			command.Connection = Connection;
-			command.CommandText = String.Format ("INSERT INTO photo_versions (photo_id, version_id, name) " +
-							     "       VALUES ({0}, {1}, '{2}')",
-							     photo.Id, version_id, SqlString (version_name));
-			command.ExecuteNonQuery ();
-			command.Dispose ();
+			query = String.Format ("INSERT INTO photo_versions (photo_id, version_id, name) " +
+					       "       VALUES ({0}, {1}, '{2}')",
+					       photo.Id, version_id, SqlString (version_name));
+			Database.ExecuteNoWait(query);
 		}
 	}
 	
@@ -1162,10 +1068,7 @@ public class PhotoStore : DbStore {
 
 	public Photo [] Query (string query)
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-		command.CommandText = query;
-		SqliteDataReader reader = command.ExecuteReader ();
+		SqliteDataReader reader = Database.Query(query);
 		
 		ArrayList version_list = new ArrayList ();
 		ArrayList id_list = new ArrayList ();
@@ -1204,8 +1107,6 @@ public class PhotoStore : DbStore {
 			//Console.WriteLine ("Skipped Loading Data");
 		}
 
-		command.Dispose ();
-
 		return id_list.ToArray (typeof (Photo)) as Photo [];
 	}
 
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/TagStore.cs f-spot/src/TagStore.cs
--- f-spot-clean/src/TagStore.cs	2006-02-17 13:11:21.000000000 +0100
+++ f-spot/src/TagStore.cs	2006-04-07 18:12:14.000000000 +0200
@@ -303,13 +303,10 @@ public class TagStore : DbStore {
 	// base class.
 	private void LoadAllTags ()
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
 		// Pass 1, get all the tags.
 
-		command.CommandText = "SELECT id, name, is_category, sort_priority, icon FROM tags";
-		SqliteDataReader reader = command.ExecuteReader ();
+		string query = "SELECT id, name, is_category, sort_priority, icon FROM tags";
+		SqliteDataReader reader = Database.Query(query);
 
 		while (reader.Read ()) {
 			uint id = Convert.ToUInt32 (reader [0]);
@@ -330,12 +327,11 @@ public class TagStore : DbStore {
 		}
 
 		reader.Close ();
-		command.Dispose ();
 
 		// Pass 2, set the parents.
 
-		command.CommandText = "SELECT id, category_id FROM tags";
-		reader = command.ExecuteReader ();
+		query = "SELECT id, category_id FROM tags";
+		reader = Database.Query(query);
 
 		while (reader.Read ()) {
 			uint id = Convert.ToUInt32 (reader [0]);
@@ -354,7 +350,6 @@ public class TagStore : DbStore {
 
 		}
 		reader.Close ();
-		command.Dispose ();
 
 		if (FSpot.Core.Database.Meta.HiddenTagId.Value != null)
 			hidden = LookupInCache ((uint) FSpot.Core.Database.Meta.HiddenTagId.ValueAsInt) as Tag;
@@ -363,10 +358,7 @@ public class TagStore : DbStore {
 
 	private void CreateTable ()
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText =
+		string query =
 			"CREATE TABLE tags (                               " +
 			"	id            INTEGER PRIMARY KEY NOT NULL," +
 			"       name          TEXT UNIQUE,                 " +
@@ -375,9 +367,7 @@ public class TagStore : DbStore {
 			"       sort_priority INTEGER,			   " +
 			"       icon          TEXT			   " +
 			")";
-
-		command.ExecuteNonQuery ();
-		command.Dispose ();
+		Database.Execute(query);
 	}
 
 
@@ -415,8 +405,8 @@ public class TagStore : DbStore {
 
 	// Constructor
 
-	public TagStore (SqliteConnection connection, bool is_new)
-		: base (connection, true)
+	public TagStore (Db database, bool is_new)
+		: base (database, true)
 	{
 		// The label for the root category is used in new and edit tag dialogs
 		root_category = new Category (null, 0, Mono.Posix.Catalog.GetString ("(None)"));
@@ -431,22 +421,15 @@ public class TagStore : DbStore {
 
 	private uint InsertTagIntoTable (Category parent_category, string name, bool is_category)
 	{
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
 		uint parent_category_id = parent_category.Id;
 
-		command.CommandText = String.Format
+		string query = String.Format
 			("INSERT INTO tags (name, category_id, is_category, sort_priority) " +
 			 "            VALUES ('{0}', {1}, {2}, 0)                          ",
 			 SqlString (name),
 			 parent_category_id,
 			 is_category ? 1 : 0);
-
-		command.ExecuteScalar ();
-		command.Dispose ();
-
-		return (uint) Connection.LastInsertRowId;
+		return Database.Execute(query);
 	}
 
 	public Tag CreateTag (Category category, string name)
@@ -498,14 +481,10 @@ public class TagStore : DbStore {
 		RemoveFromCache (item);
 		
 		((Tag)item).Category = null;
-		
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
 
-		command.CommandText = String.Format ("DELETE FROM tags WHERE id = {0}", item.Id);
-		command.ExecuteNonQuery ();
+		string query = String.Format ("DELETE FROM tags WHERE id = {0}", item.Id);
+		Database.ExecuteNoWait(query);
 
-		command.Dispose ();
 		EmitRemoved (item);
 	}
 
@@ -525,10 +504,7 @@ public class TagStore : DbStore {
 	{
 		Tag tag = item as Tag;
 
-		SqliteCommand command = new SqliteCommand ();
-		command.Connection = Connection;
-
-		command.CommandText = String.Format ("UPDATE tags SET          " +
+		string query = String.Format ("UPDATE tags SET          " +
 						     "    name = '{0}',        " +
 						     "    category_id = {1},   " +
 						     "    is_category = {2},   " +
@@ -541,10 +517,8 @@ public class TagStore : DbStore {
 						     tag.SortPriority,
 						     SqlString (GetIconString (tag)),
 						     tag.Id);
-		command.ExecuteNonQuery ();
+		Database.ExecuteNoWait(query);
 
-		command.Dispose ();
-		
 		EmitChanged (tag);
 	}
 
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/Updater.cs f-spot/src/Updater.cs
--- f-spot-clean/src/Updater.cs	2006-02-13 08:27:36.000000000 +0100
+++ f-spot/src/Updater.cs	2006-04-07 17:35:53.000000000 +0200
@@ -19,7 +19,7 @@ namespace FSpot.Database {
 			// The order these are added is important as they will be run sequentially
 			
 			// Update from version 0 to 1: Remove empty Other tags
-			AddUpdate (delegate (SqliteConnection connection) {
+			AddUpdate (delegate (Db db) {
 				string other_id = SelectSingleString ("SELECT id FROM tags WHERE name = 'Other'");
 
 				if (other_id == null)
@@ -44,26 +44,26 @@ namespace FSpot.Database {
 			});
 
 			// Update from version 1 to 2: Restore Other tags that were removed leaving dangling child tags
-			AddUpdate (delegate (SqliteConnection connection) {
+			AddUpdate (delegate (Db db) {
 				string tag_count = SelectSingleString ("SELECT COUNT(*) FROM tags WHERE category_id != 0 AND category_id NOT IN (SELECT id FROM tags)");
 
 				// If there are no dangling tags, then don't do anything
 				if (tag_count == null || System.Int32.Parse (tag_count) == 0)
 					return;
 
-				ExecuteScalar ("INSERT INTO tags (name, category_id, is_category, icon) VALUES ('Other', 0, 1, 'stock_icon:f-spot-other.png')");
+				uint id = ExecuteScalar ("INSERT INTO tags (name, category_id, is_category, icon) VALUES ('Other', 0, 1, 'stock_icon:f-spot-other.png')");
 
 				ExecuteNonQuery (String.Format (
 					@"UPDATE tags SET category_id = {0} WHERE id IN 
 					(SELECT id FROM tags WHERE category_id != 0 AND category_id 
 					NOT IN (SELECT id FROM tags))",
-					connection.LastInsertRowId));
+					id));
 
 				System.Console.WriteLine ("Other tag restored.  Sorry about that!");
 			});
 			
 			// Update from version 2 to 3
-			//AddUpdate (delegate (SqliteConnection connection) {
+			//AddUpdate (delegate (Db db) {
 			//	do update here
 			//});
 		}
@@ -157,24 +157,12 @@ namespace FSpot.Database {
 
 		private static void ExecuteNonQuery (string statement)
 		{
-			SqliteCommand command = new SqliteCommand ();
-			command.Connection = db.Connection;
-
-			command.CommandText = statement;
-
-			command.ExecuteNonQuery ();
-			command.Dispose ();
+			db.Execute(statement);
 		}
 		
-		private static void ExecuteScalar (string statement)
+		private static uint ExecuteScalar (string statement)
 		{
-			SqliteCommand command = new SqliteCommand ();
-			command.Connection = db.Connection;
-
-			command.CommandText = statement;
-
-			command.ExecuteScalar ();
-			command.Dispose ();
+			return db.Execute(statement);
 		}
 		
 		private static string SelectSingleString (string statement)
@@ -182,18 +170,12 @@ namespace FSpot.Database {
 			string result = null;
 
 			try {
-				SqliteCommand command = new SqliteCommand ();
-				command.Connection = db.Connection;
-
-				command.CommandText = statement;
-
-				SqliteDataReader reader = command.ExecuteReader ();
+				SqliteDataReader reader = db.Query(statement);
 				
 				if (reader.Read ())
 					result = reader [0].ToString ();
 
 				reader.Close ();
-				command.Dispose ();
 			} catch (Exception e) {}
 
 			return result;
@@ -218,7 +200,7 @@ namespace FSpot.Database {
 			return temp_name;
 		}
 
-		private delegate void UpdateCode (SqliteConnection connection);
+		private delegate void UpdateCode (Db db);
 
 		private class Update {
 			public int Version;
@@ -240,7 +222,7 @@ namespace FSpot.Database {
 
 			public void Execute (Db db, MetaItem db_version)
 			{
-				code (db.Connection);
+				code (db);
 				
 				Console.WriteLine ("Updated database from version {0} to {1}",
 						db_version.ValueAsInt,
diff -uprN -x CVS -x Makefile -x '*.m4' -x configure -x autom4te.cache -x 'config.*' -x 'Makefile.in*' -x install-sh -x 'intltool-*' -x '*-marshal.*' -x ltmain.sh -x missing -x mkinstalldirs f-spot-clean/src/WorkerThread.cs f-spot/src/WorkerThread.cs
--- f-spot-clean/src/WorkerThread.cs	1970-01-01 01:00:00.000000000 +0100
+++ f-spot/src/WorkerThread.cs	2006-04-07 19:16:16.000000000 +0200
@@ -0,0 +1,183 @@
+/* 
+ * WorkerThread - Executes background jobs.
+ *
+ * This thread runs in the background, performing long duration tasks without
+ * blocking the main UI. All jobs are stored in the jobs table, with a Command
+ * column to distinguish the kind of job. Job parameters are serialized in the
+ * Options string, it's up to the job execution method to deserialize this
+ * data. Scheduled jobs gracefully survive F-Spot restarts.
+ */
+
+using Mono.Data.SqliteClient;
+using System;
+using System.Threading;
+
+public class WorkerThread {
+	private bool looping = false;
+	private int ticks = 0;
+	private Db db;
+	private Thread worker = null;
+
+	public WorkerThread(Db db) {
+		this.db = db;
+	}
+
+	/*
+	 * Installs the worker thread, this is called by the Organize() method
+	 * in Core, when f-spot is idle. Will obviously be called only once.
+	 */
+	public bool Install() {
+		if (!looping) {
+			looping = true;
+			worker = new Thread(new ThreadStart(Execute));
+			worker.Priority = ThreadPriority.Lowest;
+			worker.Start();
+		}
+		return false;
+	}
+
+	/*
+	 * Execution tick of the worker thread.
+	 *
+	 * First 15 seconds are slept through, when a user has just started f-spot,
+	 * he'll probably want to use it, so we wait for a while to start doing
+	 * background work. Also, f-spot is preloading images during the first
+	 * 10 seconds on my system.
+	 */
+	public void Execute() {
+		Thread.Sleep(15000);
+		while (looping) {
+			WorkerThreadItem job = null;
+			while (looping && (job = (WorkerThreadItem)db.Jobs.GetJob()) != null) {
+				bool quick_command = false;
+				if (job.Command == "WriteMetaData") {
+					WriteMetaData(job);
+				} else {
+					Console.WriteLine("Unknown command: {0}, ignoring");
+					quick_command = true;
+				}
+				db.Jobs.Remove(job);
+
+				// Sleep a while after each command, unless it's trivial
+				if (!quick_command)
+					Thread.Sleep(10);
+			}
+
+			Thread.Sleep(10000);
+		}
+	}
+
+	public void Shutdown()
+	{
+		looping = false; // signal any running worker to stop
+		worker.Join(); // Wait for any running jobs
+	}
+
+	public void ScheduleMetaDataWrites(DbItem [] items)
+	{
+		foreach (DbItem item in items) {
+			Photo p = item as Photo; 
+			if (p != null)
+				db.Jobs.Create("WriteMetaData", String.Format("{0}", p.Id));
+		}
+	}
+
+	private void WriteMetaData(WorkerThreadItem job) 
+	{
+		uint id = Convert.ToUInt32(job.Options);
+		Photo p = db.Photos.Get(id) as Photo;
+		p.WriteMetadataToImage();
+	}
+}
+
+public class WorkerThreadItem : DbItem {
+	private string command;
+	public string Command {
+		get { return command; }
+		set { command = value; }
+	}
+
+	private string options;
+	public string Options {
+		get { return options; }
+		set { options = value; }
+	}
+
+	public WorkerThreadItem (uint id, string command, string options) : base (id)
+	{
+		this.command = command;
+		this.options = options;
+	}
+}
+
+public class WorkerThreadStore : DbStore {
+	private bool has_jobs = true;
+
+	public WorkerThreadStore (Db database, bool is_new)
+		: base (database, true)
+	{
+		// Ensure the table exists
+		bool exists = Database.TableExists("jobs");
+			
+		if (is_new || !exists)
+			CreateTable ();
+	}
+
+	private void CreateTable ()
+	{
+		Database.Execute(
+			@"CREATE TABLE jobs (
+				id		INTEGER PRIMARY KEY NOT NULL,
+				command		TEXT NOT NULL,
+				options		TEXT,
+				time		INTEGER
+			)");
+	}
+
+	public override DbItem Get (uint id)
+	{
+		return null;	// FIXME: do we need this?
+	}
+
+	public override void Remove (DbItem item)
+	{
+		string query = String.Format ("DELETE FROM jobs WHERE id = {0}", item.Id);
+		Database.ExecuteNoWait(query);
+	}
+
+	public override void Commit (DbItem dbitem)
+	{
+		// FIXME: implement, if needed
+	}
+
+	public void Create (string jobcommand, string options) {
+		string query = String.Format ("INSERT INTO jobs "
+				+ "(command, options, time) VALUES (\"{0}\", \"{1}\", \"{2}\")",
+				jobcommand, options, DateTime.Now.Ticks);
+		Database.ExecuteNoWait(query);
+		has_jobs = true;
+	}
+
+	/*
+	 * Return a job, if available, otherwise returns null.
+	 */
+	public DbItem GetJob() {
+		if (!has_jobs) 
+			return null;
+
+		WorkerThreadItem job = null;
+		string query = "SELECT id, command, options "
+				+ "FROM jobs ORDER BY time ASC LIMIT 1";
+		SqliteDataReader reader = Database.Query(query);
+
+		if (reader.Read ()) {
+			job = new WorkerThreadItem (Convert.ToUInt32(reader[0]), 
+						    reader[1].ToString(), 
+						    reader[2].ToString());
+		}
+
+		if (job == null)
+			has_jobs = false;
+		return job;
+	}
+}


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