using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using SQLitePCL; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { public abstract class BaseSqliteRepository : IDisposable { protected string DbFilePath { get; set; } protected ReaderWriterLockSlim WriteLock; protected ILogger Logger { get; private set; } protected BaseSqliteRepository(ILogger logger) { Logger = logger; WriteLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); } protected TransactionMode TransactionMode => TransactionMode.Deferred; protected TransactionMode ReadTransactionMode => TransactionMode.Deferred; internal static int ThreadSafeMode { get; set; } static BaseSqliteRepository() { SQLite3.EnableSharedCache = false; int rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MEMSTATUS, 0); //CheckOk(rc); rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1); //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SINGLETHREAD, 1); //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1); //CheckOk(rc); rc = raw.sqlite3_enable_shared_cache(1); ThreadSafeMode = raw.sqlite3_threadsafe(); } private static bool _versionLogged; private string _defaultWal; protected ManagedConnection _connection; protected virtual bool EnableSingleConnection => true; protected ManagedConnection CreateConnection(bool isReadOnly = false) { if (_connection != null) { return _connection; } lock (WriteLock) { if (!_versionLogged) { _versionLogged = true; Logger.LogInformation("Sqlite version: " + SQLite3.Version); Logger.LogInformation("Sqlite compiler options: " + string.Join(",", SQLite3.CompilerOptions.ToArray())); } ConnectionFlags connectionFlags; if (isReadOnly) { //Logger.LogInformation("Opening read connection"); //connectionFlags = ConnectionFlags.ReadOnly; connectionFlags = ConnectionFlags.Create; connectionFlags |= ConnectionFlags.ReadWrite; } else { //Logger.LogInformation("Opening write connection"); connectionFlags = ConnectionFlags.Create; connectionFlags |= ConnectionFlags.ReadWrite; } if (EnableSingleConnection) { connectionFlags |= ConnectionFlags.PrivateCache; } else { connectionFlags |= ConnectionFlags.SharedCached; } connectionFlags |= ConnectionFlags.NoMutex; var db = SQLite3.Open(DbFilePath, connectionFlags, null); try { if (string.IsNullOrWhiteSpace(_defaultWal)) { _defaultWal = db.Query("PRAGMA journal_mode").SelectScalarString().First(); Logger.LogInformation("Default journal_mode for {0} is {1}", DbFilePath, _defaultWal); } var queries = new List { //"PRAGMA cache size=-10000" //"PRAGMA read_uncommitted = true", "PRAGMA synchronous=Normal" }; if (CacheSize.HasValue) { queries.Add("PRAGMA cache_size=" + CacheSize.Value.ToString(CultureInfo.InvariantCulture)); } if (EnableTempStoreMemory) { queries.Add("PRAGMA temp_store = memory"); } else { queries.Add("PRAGMA temp_store = file"); } foreach (var query in queries) { db.Execute(query); } } catch { using (db) { } throw; } _connection = new ManagedConnection(db, false); return _connection; } } public IStatement PrepareStatement(ManagedConnection connection, string sql) { return connection.PrepareStatement(sql); } public IStatement PrepareStatementSafe(ManagedConnection connection, string sql) { return connection.PrepareStatement(sql); } public IStatement PrepareStatement(IDatabaseConnection connection, string sql) { return connection.PrepareStatement(sql); } public IStatement PrepareStatementSafe(IDatabaseConnection connection, string sql) { return connection.PrepareStatement(sql); } public List PrepareAll(IDatabaseConnection connection, IEnumerable sql) { return PrepareAllSafe(connection, sql); } public List PrepareAllSafe(IDatabaseConnection connection, IEnumerable sql) { return sql.Select(connection.PrepareStatement).ToList(); } protected bool TableExists(ManagedConnection connection, string name) { return connection.RunInTransaction(db => { using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) { foreach (var row in statement.ExecuteQuery()) { if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; }, ReadTransactionMode); } protected void RunDefaultInitialization(ManagedConnection db) { var queries = new List { "PRAGMA journal_mode=WAL", "PRAGMA page_size=4096", "PRAGMA synchronous=Normal" }; if (EnableTempStoreMemory) { queries.AddRange(new List { "pragma default_temp_store = memory", "pragma temp_store = memory" }); } else { queries.AddRange(new List { "pragma temp_store = file" }); } // Configuration and pragmas can affect VACUUM so it needs to be last. queries.Add("VACUUM"); db.ExecuteAll(string.Join(";", queries)); Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First()); } protected virtual bool EnableTempStoreMemory => false; protected virtual int? CacheSize => null; private bool _disposed; protected void CheckDisposed() { if (_disposed) { throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed."); } } public void Dispose() { _disposed = true; Dispose(true); } private readonly object _disposeLock = new object(); /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { DisposeConnection(); } } private void DisposeConnection() { try { lock (_disposeLock) { using (WriteLock.Write()) { if (_connection != null) { using (_connection) { _connection.Close(); } _connection = null; } CloseConnection(); } } } catch (Exception ex) { Logger.LogError(ex, "Error disposing database"); } } protected virtual void CloseConnection() { } protected List GetColumnNames(IDatabaseConnection connection, string table) { var list = new List(); foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) { if (row[1].SQLiteType != SQLiteType.Null) { var name = row[1].ToString(); list.Add(name); } } return list; } protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List existingColumnNames) { if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase)) { return; } connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL"); } } public static class ReaderWriterLockSlimExtensions { private sealed class ReadLockToken : IDisposable { private ReaderWriterLockSlim _sync; public ReadLockToken(ReaderWriterLockSlim sync) { _sync = sync; sync.EnterReadLock(); } public void Dispose() { if (_sync != null) { _sync.ExitReadLock(); _sync = null; } } } private sealed class WriteLockToken : IDisposable { private ReaderWriterLockSlim _sync; public WriteLockToken(ReaderWriterLockSlim sync) { _sync = sync; sync.EnterWriteLock(); } public void Dispose() { if (_sync != null) { _sync.ExitWriteLock(); _sync = null; } } } public static IDisposable Read(this ReaderWriterLockSlim obj) { //if (BaseSqliteRepository.ThreadSafeMode > 0) //{ // return new DummyToken(); //} return new WriteLockToken(obj); } public static IDisposable Write(this ReaderWriterLockSlim obj) { //if (BaseSqliteRepository.ThreadSafeMode > 0) //{ // return new DummyToken(); //} return new WriteLockToken(obj); } } }