/*************************************************************************** * QueuedSqliteDatabase.cs * * Copyright (C) 2005-2006 Novell, Inc. * Written by Aaron Bockover ****************************************************************************/ /* THIS FILE IS LICENSED UNDER THE MIT LICENSE AS OUTLINED IMMEDIATELY BELOW: * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ using System; using System.IO; using System.Threading; using System.Collections.Generic; using Mono.Data.SqliteClient; namespace Banshee.Database { /// /// A thread-safe wrapper for SQLite. /// /// /// SQLite is not thread-safe by default on Linux. Therefor, we handle /// all SQL queries on a seperate thread. /// public class QueuedSqliteDatabase : IDisposable { /// /// Holds queries to be executed. /// private Queue command_queue = new Queue(); private SqliteConnection connection; private int version; private Thread queue_thread; private volatile bool dispose_requested = false; private string dbpath; private volatile bool connected; /// /// Thread currently executing the transaction. /// /// /// Only queries from this thread are allowed (all threads are allowed /// if there is no active transaction. Queries from other threads will /// be queued until the transaction ends. /// private volatile Thread current_transaction_thread = null; /// /// Threads will sleep on this signal if there's a transaction active. /// /// /// This works as a mutual exclusion mechanism. If there are no threads /// waiting to start a transaction, the transaction_signal will /// immediately continue (note that the initial state is set to /// signalled). Otherwise, the thread will wait until signalled. private AutoResetEvent transaction_signal = new AutoResetEvent(true); /// /// Signal used to indicate there is new data in the . /// private AutoResetEvent queue_signal = new AutoResetEvent(false); public QueuedSqliteDatabase(string dbpath) { this.dbpath = dbpath; queue_thread = new Thread(ProcessQueue); queue_thread.IsBackground = true; queue_thread.Start(); } public void Dispose() { dispose_requested = true; queue_signal.Set(); queue_thread.Join(); } private void WaitForConnection() { while(!connected); } private void QueueCommand(QueuedSqliteCommand command) { // Make queries that happen outside of transactions sleep if there // is an active transaction. This uses the same thread queue as // threads willing to enter a transaction. bool release_thread = false; if (current_transaction_thread != Thread.CurrentThread) { transaction_signal.WaitOne(); release_thread = true; } lock(command_queue) { command_queue.Enqueue(command); } if (release_thread) { transaction_signal.Set(); } queue_signal.Set(); } public SqliteDataReader Query(DbCommand command) { WaitForConnection(); command.Connection = connection; command.CommandType = Banshee.Database.CommandType.Reader; QueueCommand(command); return command.WaitForResult() as SqliteDataReader; } public SqliteDataReader Query(object command) { return Query(new DbCommand(command.ToString())); } public void BeginTransaction() { if (current_transaction_thread == Thread.CurrentThread) { throw new Exception("Can't start a recursive transaction"); } transaction_signal.WaitOne(); current_transaction_thread = Thread.CurrentThread; ExecuteNonQuery("BEGIN TRANSACTION"); } public void CommitTransaction() { if (current_transaction_thread != Thread.CurrentThread) { throw new Exception("Can't commit from outside a transaction"); } ExecuteNonQuery("COMMIT TRANSACTION"); current_transaction_thread = null; transaction_signal.Set(); } public void RollbackTransaction() { if (current_transaction_thread != Thread.CurrentThread) { throw new Exception("Can't rollback from outside a transaction"); } ExecuteNonQuery("ROLLBACK"); current_transaction_thread = null; transaction_signal.Set(); } public object QuerySingle(DbCommand command) { WaitForConnection(); command.Connection = connection; command.CommandType = Banshee.Database.CommandType.Scalar; QueueCommand(command); return command.WaitForResult(); } public object QuerySingle(object command) { return QuerySingle(new DbCommand(command.ToString())); } public int Execute(DbCommand command) { WaitForConnection(); command.Connection = connection; command.CommandType = Banshee.Database.CommandType.Execute; QueueCommand(command); command.WaitForResult(); return command.InsertID; } public int Execute(object command) { return Execute(new DbCommand(command.ToString())); } public void ExecuteNonQuery(DbCommand command) { WaitForConnection(); command.Connection = connection; command.CommandType = Banshee.Database.CommandType.ExecuteNonQuery; QueueCommand(command); } public void ExecuteNonQuery(object command) { ExecuteNonQuery(new DbCommand(command.ToString())); } public bool TableExists(string table) { bool result; if (version == 2) { // Old SQLite doesn't support selecting from sqlite_master try { Execute("SELECT * FROM "+table+" LIMIT 0"); result = true; } catch (Exception) { result = false; } } else { result = Convert.ToInt32(QuerySingle(String.Format(@" SELECT COUNT(*) FROM sqlite_master WHERE Type='table' AND Name='{0}'", table))) > 0; } return result; } private void ProcessQueue() { // Connect if(connection == null) { version = GetFileVersion(dbpath); if (version == 3) { connection = new SqliteConnection("Version=3,URI=file:" + dbpath); } else if (version == 2) { connection = new SqliteConnection("Version=2,encoding=UTF-8,URI=file:" + dbpath); } else { throw new Exception("Unsupported SQLite database version"); } connection.Open(); connected = true; } // Keep handling queries while(!dispose_requested) { while(command_queue.Count > 0) { QueuedSqliteCommand command; lock(command_queue) { command = command_queue.Dequeue(); } command.Execute(); } queue_signal.WaitOne(); } // Finish connection.Close(); } private int GetFileVersion(string path) { if (!File.Exists(path)) { return 3; } using (Stream stream = File.OpenRead (path)) { byte [] data = new byte [15]; stream.Read (data, 0, data.Length); string magic = System.Text.Encoding.ASCII.GetString (data, 0, data.Length); switch (magic) { case "SQLite format 3": return 3; case "** This file co": return 2; default: return -1; } } } } public enum CommandType { Reader, Scalar, Execute, ExecuteNonQuery } public class QueuedSqliteCommand { private CommandType command_type; private object result; private int insert_id; private Exception execution_exception; private bool finished = false; private SqliteCommand command; private const int MAX_RETRIES = 4; private const int SLEEP_TIME = 1000; // 1 sec private int attempt = 0; public QueuedSqliteCommand(string query) { this.command = new SqliteCommand(query); } public QueuedSqliteCommand(SqliteConnection connection, string query, CommandType commandType) { this.command_type = commandType; this.command = new SqliteCommand(query, connection); } public void Execute() { if(result != null) { throw new ApplicationException("Command has alread been executed"); } try { switch(command_type) { case Banshee.Database.CommandType.Reader: result = command.ExecuteReader(); break; case Banshee.Database.CommandType.Scalar: result = command.ExecuteScalar(); break; case Banshee.Database.CommandType.Execute: default: result = command.ExecuteNonQuery(); insert_id = command.LastInsertRowID(); break; } } catch (SqliteBusyException) { if (attempt > MAX_RETRIES) { throw; // FIXME: show this to the user } attempt++; Thread.Sleep(SLEEP_TIME); Execute(); } catch(Exception e) { execution_exception = e; if (command_type == Banshee.Database.CommandType.ExecuteNonQuery) { throw execution_exception; } } command.Dispose(); 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 int InsertID { get { return insert_id; } } public new CommandType CommandType { get { return command_type; } set { command_type = value; } } public SqliteConnection Connection { set { command.Connection = value; } } protected SqliteParameterCollection Parameters { get { return command.Parameters; } } public string CommandText { get { return command.CommandText; } } } public class DbCommand : QueuedSqliteCommand { public DbCommand(string command) : base(command) { } public DbCommand(string command, params object [] parameters) : this(command) { for(int i = 0; i < parameters.Length;) { SqliteParameter param; if(parameters[i] is SqliteParameter) { param = (SqliteParameter)parameters[i]; if(i < parameters.Length - 1 && !(parameters[i + 1] is SqliteParameter)) { param.Value = parameters[i + 1]; i += 2; } else { i++; } } else { param = new SqliteParameter(); param.ParameterName = (string)parameters[i]; param.Value = parameters[i + 1]; i += 2; } Parameters.Add(param); } } public void AddParameter(string name, T value) { AddParameter(new DbParameter(name), value); } public void AddParameter(DbParameter param, T value) { param.Value = value; Parameters.Add(param); } } public class DbParameter : SqliteParameter { public DbParameter(string name) : base() { ParameterName = name; } public new T Value { get { return (T)base.Value; } set { base.Value = value; } } } }