using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { public abstract class BaseSqliteRepository : IDisposable { private bool _disposed = false; protected BaseSqliteRepository(ILogger logger) { Logger = logger; } /// /// Gets or sets the path to the DB file. /// /// Path to the DB file. protected string DbFilePath { get; set; } /// /// Gets the logger. /// /// The logger. protected ILogger Logger { get; } /// /// Gets the default connection flags. /// /// The default connection flags. protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex; /// /// Gets the transaction mode. /// /// The transaction mode.> protected TransactionMode TransactionMode => TransactionMode.Deferred; /// /// Gets the transaction mode for read-only operations. /// /// The transaction mode. protected TransactionMode ReadTransactionMode => TransactionMode.Deferred; /// /// Gets the cache size. /// /// The cache size or null. protected virtual int? CacheSize => null; /// /// Gets the journal mode. https://www.sqlite.org/pragma.html#pragma_journal_mode /// /// The journal mode. protected virtual string JournalMode => "TRUNCATE"; /// /// Gets the page size. /// /// The page size or null. protected virtual int? PageSize => null; /// /// Gets the temp store mode. /// /// The temp store mode. /// protected virtual TempStoreMode TempStore => TempStoreMode.Default; /// /// Gets the synchronous mode. /// /// The synchronous mode or null. /// protected virtual SynchronousMode? Synchronous => null; /// /// Gets or sets the write lock. /// /// The write lock. protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1); /// /// Gets or sets the write connection. /// /// The write connection. protected SQLiteDatabaseConnection WriteConnection { get; set; } protected ManagedConnection GetConnection(bool _ = false) { WriteLock.Wait(); if (WriteConnection != null) { return new ManagedConnection(WriteConnection, WriteLock); } WriteConnection = SQLite3.Open( DbFilePath, DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite, null); if (CacheSize.HasValue) { WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value); } if (!string.IsNullOrWhiteSpace(JournalMode)) { WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode); } if (Synchronous.HasValue) { WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); } if (PageSize.HasValue) { WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value); } WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore); // Configuration and pragmas can affect VACUUM so it needs to be last. WriteConnection.Execute("VACUUM"); return new ManagedConnection(WriteConnection, WriteLock); } public IStatement PrepareStatement(ManagedConnection connection, string sql) => connection.PrepareStatement(sql); public IStatement PrepareStatement(IDatabaseConnection connection, string sql) => connection.PrepareStatement(sql); public IEnumerable PrepareAll(IDatabaseConnection connection, IEnumerable sql) => sql.Select(connection.PrepareStatement); 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 List GetColumnNames(IDatabaseConnection connection, string table) { var columnNames = new List(); foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) { if (row[1].SQLiteType != SQLiteType.Null) { var name = row[1].ToString(); columnNames.Add(name); } } return columnNames; } 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"); } protected void CheckDisposed() { if (_disposed) { throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed."); } } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// 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 (_disposed) { return; } if (dispose) { WriteLock.Wait(); try { WriteConnection?.Dispose(); } finally { WriteLock.Release(); } WriteLock.Dispose(); } WriteConnection = null; WriteLock = null; _disposed = true; } } /// /// The disk synchronization mode, controls how aggressively SQLite will write data /// all the way out to physical storage. /// public enum SynchronousMode { /// /// SQLite continues without syncing as soon as it has handed data off to the operating system /// Off = 0, /// /// SQLite database engine will still sync at the most critical moments /// Normal = 1, /// /// SQLite database engine will use the xSync method of the VFS /// to ensure that all content is safely written to the disk surface prior to continuing. /// Full = 2, /// /// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal /// is synced after that journal is unlinked to commit a transaction in DELETE mode. /// Extra = 3 } /// /// Storage mode used by temporary database files. /// public enum TempStoreMode { /// /// The compile-time C preprocessor macro SQLITE_TEMP_STORE /// is used to determine where temporary tables and indices are stored. /// Default = 0, /// /// Temporary tables and indices are stored in a file. /// File = 1, /// /// Temporary tables and indices are kept in as if they were pure in-memory databases memory. /// Memory = 2 } }