@ -1,183 +1,141 @@
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 ; }
private bool _disposed = false ;
protected BaseSqliteRepository ( ILogger logger )
{
Logger = logger ;
WriteLock = new ReaderWriterLockSlim ( LockRecursionPolicy . NoRecursion ) ;
}
protected TransactionMode TransactionMode = > TransactionMode . Deferred ;
/// <summary>
/// Gets or sets the path to the DB file.
/// </summary>
/// <value>Path to the DB file.</value>
protected string DbFilePath { get ; set ; }
protected TransactionMode ReadTransactionMode = > TransactionMode . Deferred ;
/// <summary>
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
protected ILogger Logger { get ; }
internal static int ThreadSafeMode { get ; set ; }
/// <summary>
/// Gets the default connection flags.
/// </summary>
/// <value>The default connection flags.</value>
protected virtual ConnectionFlags DefaultConnectionFlags = > ConnectionFlags . NoMutex ;
static BaseSqliteRepository ( )
{
SQLite3 . EnableSharedCache = false ;
/// <summary>
/// Gets the transaction mode.
/// </summary>
/// <value>The transaction mode.</value>>
protected TransactionMode TransactionMode = > TransactionMode . Deferred ;
int rc = raw . sqlite3_config ( raw . SQLITE_CONFIG_MEMSTATUS , 0 ) ;
//CheckOk(rc);
/// <summary>
/// Gets the transaction mode for read-only operations.
/// </summary>
/// <value>The transaction mode.</value>
protected TransactionMode ReadTransactionMode = > TransactionMode . Deferred ;
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);
/// <summary>
/// Gets the cache size.
/// </summary>
/// <value>The cache size or null.</value>
protected virtual int? CacheSize = > null ;
rc = raw . sqlite3_enable_shared_cache ( 1 ) ;
/// <summary>
/// Gets the journal mode.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode = > "WAL" ;
ThreadSafeMode = raw . sqlite3_threadsafe ( ) ;
}
/// <summary>
/// Gets the page size.
/// </summary>
/// <value>The page size or null.</value>
protected virtual int? PageSize = > null ;
/// <summary>
/// Gets the temp store mode.
/// </summary>
/// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore = > TempStoreMode . Default ;
private static bool _versionLogged ;
/// <summary>
/// Gets the synchronous mode.
/// </summary>
/// <value>The synchronous mode or null.</value>
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode ? Synchronous = > null ;
private string _defaultWal ;
protected ManagedConnection _connection ;
/// <summary>
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected SemaphoreSlim WriteLock { get ; set ; } = new SemaphoreSlim ( 1 , 1 ) ;
protected virtual bool EnableSingleConnection = > true ;
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get ; set ; }
protected ManagedConnection CreateConnection ( bool isReadOnly = false )
protected ManagedConnection GetConnection( bool _ = false )
{
if ( _connection ! = null )
WriteLock . Wait ( ) ;
if ( WriteConnection ! = null )
{
return _connection ;
return new ManagedConnection ( WriteConnection , WriteLock ) ;
}
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 ( ) ;
WriteConnection = SQLite3 . Open (
DbFilePath ,
DefaultConnectionFlags | ConnectionFlags . Create | ConnectionFlags . ReadWrite ,
null ) ;
Logger . LogInformation ( "Default journal_mode for {0} is {1}" , DbFilePath , _defaultWal ) ;
}
var queries = new List < string >
{
//"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" ) ;
}
if ( CacheSize . HasValue )
{
WriteConnection . Execute ( "PRAGMA cache_size=" + CacheSize . Value ) ;
}
foreach ( var query in queries )
{
db . Execute ( query ) ;
}
}
catch
{
using ( db )
{
if ( ! string . IsNullOrWhiteSpace ( JournalMode ) )
{
WriteConnection . Execute ( "PRAGMA journal_mode=" + JournalMode ) ;
}
}
if ( Synchronous . HasValue )
{
WriteConnection . Execute ( "PRAGMA synchronous=" + ( int ) Synchronous . Value ) ;
}
throw ;
}
if ( PageSize . HasValue )
{
WriteConnection . Execute ( "PRAGMA page_size=" + PageSize . Value ) ;
}
_connection = new ManagedConnection ( db , false ) ;
WriteConnection . Execute ( "PRAGMA temp_store=" + ( int ) TempStore ) ;
return _connection ;
}
return new ManagedConnection ( WriteConnection , WriteLock ) ;
}
public IStatement PrepareStatement ( ManagedConnection connection , string sql )
{
return connection . PrepareStatement ( sql ) ;
}
public IStatement PrepareStatementSafe ( ManagedConnection connection , string sql )
{
return connection . PrepareStatement ( sql ) ;
}
= > 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 < IStatement > PrepareAll ( IDatabaseConnection connection , IEnumerable < string > sql )
{
return PrepareAllSafe ( connection , sql ) ;
}
= > connection . PrepareStatement ( sql ) ;
public List < IStatement > PrepareAllSafe ( IDatabaseConnection connection , IEnumerable < string > sql )
{
return sql . Select ( connection . PrepareStatement ) . ToList ( ) ;
}
public IEnumerable < IStatement > PrepareAll ( IDatabaseConnection connection , IEnumerable < string > sql )
= > sql . Select ( connection . PrepareStatement ) ;
protected bool TableExists ( ManagedConnection connection , string name )
{
@ -199,40 +157,33 @@ namespace Emby.Server.Implementations.Data
} , ReadTransactionMode ) ;
}
protected void RunDefaultInitialization ( ManagedConnection db )
protected List < string > GetColumnNames ( IDatabaseConnection connection , string table )
{
var queries = new List < string >
{
"PRAGMA journal_mode=WAL" ,
"PRAGMA page_size=4096" ,
"PRAGMA synchronous=Normal"
} ;
var columnNames = new List < string > ( ) ;
if ( EnableTempStoreMemory )
{
queries . AddRange ( new List < string >
{
"pragma default_temp_store = memory" ,
"pragma temp_store = memory"
} ) ;
}
else
foreach ( var row in connection . Query ( "PRAGMA table_info(" + table + ")" ) )
{
queries . AddRange ( new List < string >
if ( row [ 1 ] . SQLiteType ! = SQLiteType . Null )
{
"pragma temp_store = file"
} ) ;
var name = row [ 1 ] . ToString ( ) ;
columnNames . Add ( name ) ;
}
}
db . ExecuteAll ( string . Join ( ";" , queries ) ) ;
Logger . LogInformation ( "PRAGMA synchronous=" + db . Query ( "PRAGMA synchronous" ) . SelectScalarString ( ) . First ( ) ) ;
return columnNames ;
}
protected virtual bool EnableTempStoreMemory = > false ;
protected void AddColumn ( IDatabaseConnection connection , string table , string columnName , string type , List < string > existingColumnNames )
{
if ( existingColumnNames . Contains ( columnName , StringComparer . OrdinalIgnoreCase ) )
{
return ;
}
protected virtual int? CacheSize = > null ;
connection . Execute ( "alter table " + table + " add column " + columnName + " " + type + " NULL" ) ;
}
private bool _disposed ;
protected void CheckDisposed ( )
{
if ( _disposed )
@ -241,139 +192,94 @@ namespace Emby.Server.Implementations.Data
}
}
/// <inheritdoc />
public void Dispose ( )
{
_disposed = true ;
Dispose ( true ) ;
GC . SuppressFinalize ( this ) ;
}
private readonly object _disposeLock = new object ( ) ;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose ( bool dispose )
{
if ( dispose)
if ( _ disposed )
{
DisposeConnection ( ) ;
return ;
}
}
private void DisposeConnection ( )
{
try
if ( dispose )
{
lock ( _disposeLock )
WriteLock . Wait ( ) ;
try
{
using ( WriteLock . Write ( ) )
{
if ( _connection ! = null )
{
using ( _connection )
{
_connection . Close ( ) ;
}
_connection = null ;
}
CloseConnection ( ) ;
}
WriteConnection . Dispose ( ) ;
}
finally
{
WriteLock . Release ( ) ;
}
WriteLock . Dispose ( ) ;
}
catch ( Exception ex )
{
Logger . LogError ( ex , "Error disposing database" ) ;
}
}
protected virtual void CloseConnection ( )
{
WriteConnection = null ;
WriteLock = null ;
_disposed = true ;
}
}
protected List < string > GetColumnNames ( IDatabaseConnection connection , string table )
{
var list = new List < string > ( ) ;
foreach ( var row in connection . Query ( "PRAGMA table_info(" + table + ")" ) )
{
if ( row [ 1 ] . SQLiteType ! = SQLiteType . Null )
{
var name = row [ 1 ] . ToString ( ) ;
list . Add ( name ) ;
}
}
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system
/// </summary>
Off = 0 ,
return list ;
}
/// <summary>
/// SQLite database engine will still sync at the most critical moments
/// </summary>
Normal = 1 ,
protected void AddColumn ( IDatabaseConnection connection , string table , string columnName , string type , List < string > existingColumnNames )
{
if ( existingColumnNames . Contains ( columnName , StringComparer . OrdinalIgnoreCase ) )
{
return ;
}
/// <summary>
/// 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.
/// </summary>
Full = 2 ,
connection . Execute ( "alter table " + table + " add column " + columnName + " " + type + " NULL" ) ;
}
/// <summary>
/// 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.
/// </summary>
Extra = 3
}
public static class ReaderWriterLockSlimExtensions
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
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 ;
}
}
}
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0 ,
public static IDisposable Read ( this ReaderWriterLockSlim obj )
{
//if (BaseSqliteRepository.ThreadSafeMode > 0)
//{
// return new DummyToken();
//}
return new WriteLockToken ( obj ) ;
}
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1 ,
public static IDisposable Write ( this ReaderWriterLockSlim obj )
{
//if (BaseSqliteRepository.ThreadSafeMode > 0)
//{
// return new DummyToken();
//}
return new WriteLockToken ( obj ) ;
}
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}
}