diff --git a/NzbDrone.Core.Test/App.config b/NzbDrone.Core.Test/App.config index 88b220807..6b4327abb 100644 --- a/NzbDrone.Core.Test/App.config +++ b/NzbDrone.Core.Test/App.config @@ -8,4 +8,12 @@ type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" /> + + + + + + + + \ No newline at end of file diff --git a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index aad12a791..2a5407133 100644 --- a/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -59,6 +59,18 @@ ..\packages\Unity.2.0\lib\20\Microsoft.Practices.Unity.Interception.Configuration.dll + + False + ..\packages\MigratorDotNet.0.9.0.28138\lib\Net40\Migrator.dll + + + False + ..\packages\MigratorDotNet.0.9.0.28138\lib\Net40\Migrator.Framework.dll + + + False + ..\packages\MigratorDotNet.0.9.0.28138\lib\Net40\Migrator.Providers.dll + ..\packages\Moq.4.0.10827\lib\NET40\Moq.dll @@ -100,6 +112,7 @@ + diff --git a/NzbDrone.Core.Test/RepositoryProviderTest.cs b/NzbDrone.Core.Test/RepositoryProviderTest.cs new file mode 100644 index 000000000..f2bd21428 --- /dev/null +++ b/NzbDrone.Core.Test/RepositoryProviderTest.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Text; +using Gallio.Framework; +using MbUnit.Framework; +using MbUnit.Framework.ContractVerifiers; +using Migrator.Framework; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Repository; +using NzbDrone.Core.Repository.Quality; +using NzbDrone.Core.Test.Framework; +using SubSonic.DataProviders; +using SubSonic.Repository; +using SubSonic.Schema; +using SubSonic.SqlGeneration.Schema; + +namespace NzbDrone.Core.Test +{ + [TestFixture] + // ReSharper disable InconsistentNaming + public class RepositoryProviderTest + { + [Test] + public void Get_Assembly_repos() + { + var provider = new RepositoryProvider(); + var types = provider.GetRepositoryTypes(); + + Assert.IsNotEmpty(types); + Assert.Contains(types, typeof(Config)); + Assert.Contains(types, typeof(Episode)); + Assert.Contains(types, typeof(EpisodeFile)); + Assert.Contains(types, typeof(ExternalNotificationSetting)); + Assert.Contains(types, typeof(History)); + Assert.Contains(types, typeof(IndexerSetting)); + Assert.Contains(types, typeof(JobSetting)); + Assert.Contains(types, typeof(RootDir)); + Assert.Contains(types, typeof(Season)); + Assert.Contains(types, typeof(Series)); + + Assert.Contains(types, typeof(QualityProfile)); + + Assert.DoesNotContain(types, typeof(QualityTypes)); + } + + + + + + [Test] + public void Get_table_columns() + { + var provider = new RepositoryProvider(); + var typeTable = provider.GetSchemaFromType(typeof(TestRepoType)); + + Assert.IsNotNull(typeTable.Columns); + Assert.Count(3, typeTable.Columns); + Assert.AreEqual("TestRepoTypes", typeTable.Name); + } + + [Test] + public void ConvertToMigratorColumn() + { + var provider = new RepositoryProvider(); + + var subsonicColumn = new DatabaseColumn + { + Name = "Name", + DataType = DbType.Boolean, + IsPrimaryKey = true, + IsNullable = true + }; + + var migColumn = provider.ConvertToMigratorColumn(subsonicColumn); + + Assert.IsTrue(migColumn.IsPrimaryKey); + Assert.AreEqual(ColumnProperty.Null | ColumnProperty.PrimaryKey, migColumn.ColumnProperty); + } + + + [Test] + public void GetDbColumns() + { + string connectionString = "Data Source=" + Guid.NewGuid() + ".db;Version=3;New=True"; + var dbProvider = ProviderFactory.GetProvider(connectionString, "System.Data.SQLite"); + var repo = new SimpleRepository(dbProvider, SimpleRepositoryOptions.RunMigrations); + + repo.Add(new TestRepoType(){Value = "Dummy"}); + + var repositoryProvider = new RepositoryProvider(); + var columns = repositoryProvider.GetColumnsFromDatabase(connectionString, "TestRepoTypes"); + + Assert.Count(3, columns); + + } + + + [Test] + public void DeleteColumns() + { + string connectionString = "Data Source=" + Guid.NewGuid() + ".db;Version=3;New=True"; + var dbProvider = ProviderFactory.GetProvider(connectionString, "System.Data.SQLite"); + var repo = new SimpleRepository(dbProvider, SimpleRepositoryOptions.RunMigrations); + + repo.Add(new TestRepoType(){Value = "Dummy"}); + + var repositoryProvider = new RepositoryProvider(); + var typeSchema = repositoryProvider.GetSchemaFromType(typeof(TestRepoType2)); + var columns = repositoryProvider.GetColumnsFromDatabase(connectionString, "TestRepoTypes"); + + + var deletedColumns = repositoryProvider.GetDeletedColumns(typeSchema, columns); + + + Assert.Count(1, deletedColumns); + Assert.AreEqual("NewName", deletedColumns[0].Name.Trim('[', ']')); + } + + + [Test] + public void NewColumns() + { + string connectionString = "Data Source=" + Guid.NewGuid() + ".db;Version=3;New=True"; + var dbProvider = ProviderFactory.GetProvider(connectionString, "System.Data.SQLite"); + var repo = new SimpleRepository(dbProvider, SimpleRepositoryOptions.RunMigrations); + + repo.Add(new TestRepoType2() { Value = "dummy" }); + + var repositoryProvider = new RepositoryProvider(); + var typeSchema = repositoryProvider.GetSchemaFromType(typeof(TestRepoType)); + var columns = repositoryProvider.GetColumnsFromDatabase(connectionString, "TestRepoType2s"); + + + var deletedColumns = repositoryProvider.GetNewColumns(typeSchema, columns); + + + Assert.Count(1, deletedColumns); + Assert.AreEqual("NewName", deletedColumns[0].Name.Trim('[', ']')); + } + + } + + + public class TestRepoType + { + [SubSonicPrimaryKey] + public int TestId { get; set; } + + [SubSonicColumnNameOverride("NewName")] + public Boolean BaddBoolean { get; set; } + + + public string Value { get; set; } + + [SubSonicIgnore] + public Boolean BaddBooleanIgnored { get; set; } + } + + + public class TestRepoType2 + { + [SubSonicPrimaryKey] + public int TestId { get; set; } + + public string Value { get; set; } + + [SubSonicIgnore] + public Boolean BaddBooleanIgnored { get; set; } + } +} diff --git a/NzbDrone.Core/CentralDispatch.cs b/NzbDrone.Core/CentralDispatch.cs index 8c60db6ff..1b299fb4d 100644 --- a/NzbDrone.Core/CentralDispatch.cs +++ b/NzbDrone.Core/CentralDispatch.cs @@ -6,6 +6,7 @@ using System.IO; using System.Web.Hosting; using Ninject; using NLog; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Providers; using NzbDrone.Core.Providers.Core; @@ -43,12 +44,28 @@ namespace NzbDrone.Core { if (_kernel == null) { - BindKernel(); + InitializeApp(); } return _kernel; } } + private static void InitializeApp() + { + BindKernel(); + + LogConfiguration.Setup(); + + Migrations.Run(); + ForceMigration(_kernel.Get()); + + SetupDefaultQualityProfiles(_kernel.Get()); //Setup the default QualityProfiles on start-up + + BindIndexers(); + BindJobs(); + BindExternalNotifications(); + } + public static void BindKernel() { lock (KernelLock) @@ -56,27 +73,7 @@ namespace NzbDrone.Core Logger.Debug("Binding Ninject's Kernel"); _kernel = new StandardKernel(); - //Sqlite - var appDataPath = new DirectoryInfo(Path.Combine(AppPath, "App_Data")); - if (!appDataPath.Exists) appDataPath.Create(); - - string connectionString = String.Format("Data Source={0};Version=3;", - Path.Combine(appDataPath.FullName, "nzbdrone.db")); - var dbProvider = ProviderFactory.GetProvider(connectionString, "System.Data.SQLite"); - - string logConnectionString = String.Format("Data Source={0};Version=3;", - Path.Combine(appDataPath.FullName, "log.db")); - var logDbProvider = ProviderFactory.GetProvider(logConnectionString, "System.Data.SQLite"); - - - //SQLExpress - //string logConnectionString = String.Format(@"server=.\SQLExpress; database=NzbDroneLogs; Trusted_Connection=True;"); - //var logDbProvider = ProviderFactory.GetProvider(logConnectionString, "System.Data.SqlClient"); - var logRepository = new SimpleRepository(logDbProvider, SimpleRepositoryOptions.RunMigrations); - //dbProvider.ExecuteQuery(new QueryCommand("VACUUM", dbProvider)); - //dbProvider.Log = new NlogWriter(); - _kernel.Bind().ToSelf().InSingletonScope(); _kernel.Bind().ToSelf().InTransientScope(); _kernel.Bind().ToSelf().InSingletonScope(); @@ -100,21 +97,10 @@ namespace NzbDrone.Core _kernel.Bind().ToSelf().InSingletonScope(); _kernel.Bind().ToSelf().InSingletonScope(); _kernel.Bind().ToSelf().InSingletonScope(); - _kernel.Bind().ToMethod( - c => new SimpleRepository(dbProvider, SimpleRepositoryOptions.RunMigrations)).InSingletonScope(); - - _kernel.Bind().ToConstant(logRepository).WhenInjectedInto(). - InSingletonScope(); - _kernel.Bind().ToConstant(logRepository).WhenInjectedInto().InSingletonScope(); - - LogConfiguration.Setup(); - - ForceMigration(_kernel.Get()); - SetupDefaultQualityProfiles(_kernel.Get()); //Setup the default QualityProfiles on start-up - BindIndexers(); - BindJobs(); - BindExternalNotifications(); + _kernel.Bind().ToConstant(Connection.MainDataRepository).InSingletonScope(); + _kernel.Bind().ToConstant(Connection.LogDataRepository).WhenInjectedInto().InSingletonScope(); + _kernel.Bind().ToConstant(Connection.LogDataRepository).WhenInjectedInto().InSingletonScope(); } } diff --git a/NzbDrone.Core/Datastore/Connection.cs b/NzbDrone.Core/Datastore/Connection.cs new file mode 100644 index 000000000..e8e7b6a10 --- /dev/null +++ b/NzbDrone.Core/Datastore/Connection.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using SubSonic.DataProviders; +using SubSonic.Repository; + +namespace NzbDrone.Core.Datastore +{ + public static class Connection + { + private static readonly DirectoryInfo AppDataPath = new DirectoryInfo(Path.Combine(CentralDispatch.AppPath, "App_Data")); + + static Connection() + { + if (!AppDataPath.Exists) AppDataPath.Create(); + } + + + public static String MainConnectionString + { + get + { + return String.Format("Data Source={0};Version=3;", Path.Combine(AppDataPath.FullName, "nzbdrone.db")); + } + } + + public static String LogConnectionString + { + get + { + return String.Format("Data Source={0};Version=3;", Path.Combine(AppDataPath.FullName, "log.db")); + } + } + + + private static IDataProvider _mainDataProvider; + public static IDataProvider MainDataProvider + { + get + { + if (_mainDataProvider == null) + { + _mainDataProvider = ProviderFactory.GetProvider(Connection.MainConnectionString, "System.Data.SQLite"); + } + return _mainDataProvider; + } + + } + + private static IDataProvider _logDataProvider; + public static IDataProvider LogDataProvider + { + get + { + if (_logDataProvider == null) + { + _logDataProvider = ProviderFactory.GetProvider(Connection.LogConnectionString, "System.Data.SQLite"); + } + return _logDataProvider; + } + + } + + + private static SimpleRepository _mainDataRepository; + public static SimpleRepository MainDataRepository + { + get + { + if (_mainDataRepository == null) + { + _mainDataRepository = new SimpleRepository(MainDataProvider, SimpleRepositoryOptions.RunMigrations); + } + + return _mainDataRepository; + } + + } + + private static SimpleRepository _logDataRepository; + public static SimpleRepository LogDataRepository + { + get + { + if (_logDataRepository == null) + { + _logDataRepository = new SimpleRepository(LogDataProvider, SimpleRepositoryOptions.RunMigrations); + } + return _logDataRepository; + } + + } + + + + } +} diff --git a/NzbDrone.Core/Datastore/MigrationLogger.cs b/NzbDrone.Core/Datastore/MigrationLogger.cs new file mode 100644 index 000000000..f9adaa38e --- /dev/null +++ b/NzbDrone.Core/Datastore/MigrationLogger.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Migrator.Framework; +using NLog; + +namespace NzbDrone.Core.Datastore +{ + class MigrationLogger : ILogger + { + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public void Started(List currentVersion, long finalVersion) + { + Logger.Info("Starting Datastore migration {0} -> {1}", String.Join(",", currentVersion), finalVersion); + } + + public void MigrateUp(long version, string migrationName) + { + Logger.Info("Starting MigrateUp {0} [{1}]", version, migrationName); + } + + public void MigrateDown(long version, string migrationName) + { + Logger.Info("Starting MigrateDown {0} [{1}]", version, migrationName); + } + + public void Skipping(long version) + { + Logger.Info("Skipping MigrateDown {0}", version); + } + + public void RollingBack(long originalVersion) + { + Logger.Info("Rolling Back to {0}", originalVersion); + } + + public void ApplyingDBChange(string sql) + { + Logger.Info("Applying DB Change {0}", sql); + } + + public void Exception(long version, string migrationName, Exception ex) + { + Logger.ErrorException(migrationName + " " + version, ex); + } + + public void Exception(string message, Exception ex) + { + Logger.ErrorException(message, ex); + } + + public void Finished(List currentVersion, long finalVersion) + { + Logger.Info("Finished Datastore migration {0} -> {1}", String.Join(",", currentVersion), finalVersion); + } + + public void Log(string format, params object[] args) + { + Logger.Info(format, args); + } + + public void Warn(string format, params object[] args) + { + Logger.Warn(format, args); + } + + public void Trace(string format, params object[] args) + { + Logger.Trace(format, args); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Datastore/Migrations.cs b/NzbDrone.Core/Datastore/Migrations.cs new file mode 100644 index 000000000..a3e27791a --- /dev/null +++ b/NzbDrone.Core/Datastore/Migrations.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Text; +using Migrator.Framework; +using NLog; +using SubSonic.Extensions; +using SubSonic.Schema; + +namespace NzbDrone.Core.Datastore +{ + public class Migrations + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static void Run() + { + Logger.Info("Preparing to migrate databse"); + + try + { + var mig = new Migrator.Migrator("Sqlite", Connection.MainConnectionString, + Assembly.GetAssembly(typeof(Migrations)), true, new MigrationLogger()); + + mig.MigrateToLastVersion(); + + Logger.Info("Database migration completed"); + } + catch (Exception e) + { + Logger.FatalException("An error has occured while migrating database", e); + } + } + + + public static void RemoveDeletedColumns(ITransformationProvider transformationProvider) + { + var provider = new RepositoryProvider(); + var repoTypes = provider.GetRepositoryTypes(); + + foreach (var repoType in repoTypes) + { + var typeSchema = provider.GetSchemaFromType(repoType); + var dbColumns = provider.GetColumnsFromDatabase(Connection.MainConnectionString, typeSchema.Name); + + var deletedColumns = provider.GetDeletedColumns(typeSchema, dbColumns); + + foreach (var deletedColumn in deletedColumns) + { + Logger.Info("Removing column '{0}' from '{1}'", deletedColumn.Name, repoType.Name); + transformationProvider.RemoveColumn(typeSchema.Name, deletedColumn.Name); + } + + } + + } + + public static void AddNewColumns(ITransformationProvider transformationProvider) + { + var provider = new RepositoryProvider(); + var repoTypes = provider.GetRepositoryTypes(); + + foreach (var repoType in repoTypes) + { + var typeSchema = provider.GetSchemaFromType(repoType); + var dbColumns = provider.GetColumnsFromDatabase(Connection.MainConnectionString, typeSchema.Name); + + var newColumns = provider.GetNewColumns(typeSchema, dbColumns); + + foreach (var newColumn in newColumns) + { + Logger.Info("Adding column '{0}' to '{1}'", newColumn.Name, repoType.Name); + transformationProvider.AddColumn(typeSchema.Name, newColumn); + } + + } + + } + + } + + [Migration(20110523)] + public class Migration20110523 : Migration + { + public override void Up() + { + Migrations.RemoveDeletedColumns(Database); + Migrations.AddNewColumns(Database); + } + + public override void Down() + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Datastore/RepositoryProvider.cs b/NzbDrone.Core/Datastore/RepositoryProvider.cs new file mode 100644 index 000000000..c4cc3da67 --- /dev/null +++ b/NzbDrone.Core/Datastore/RepositoryProvider.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using Migrator.Providers; +using Migrator.Providers.SQLite; +using SubSonic.DataProviders; +using SubSonic.Extensions; +using SubSonic.Schema; +using Migrator.Framework; + + +namespace NzbDrone.Core.Datastore +{ + public class RepositoryProvider + { + public virtual IList GetRepositoryTypes() + { + var coreAssembly = Assembly.GetExecutingAssembly(); + var repoTypes = coreAssembly.GetTypes().Where(t => !String.IsNullOrWhiteSpace(t.Namespace) && t.Namespace.StartsWith("NzbDrone.Core.Repository")); + + repoTypes = repoTypes.Where(r => !r.IsEnum); + return repoTypes.ToList(); + } + + public virtual ITable GetSchemaFromType(Type type) + { + return type.ToSchemaTable(Connection.MainDataProvider); + } + + public virtual Column[] GetColumnsFromDatabase(string connectionString, string tableName) + { + var dialact = new SQLiteDialect(); + var mig = new SQLiteTransformationProvider(dialact, connectionString); + + return mig.GetColumns(tableName); + } + + + public virtual List GetDeletedColumns(ITable typeSchema, Column[] dbColumns) + { + var deleteColumns = new List(); + foreach (var dbColumn in dbColumns) + { + if (!typeSchema.Columns.ToList().Exists(c => c.Name == dbColumn.Name.Trim('[', ']'))) + { + deleteColumns.Add(dbColumn); + } + } + + return deleteColumns; + } + + + public virtual List GetNewColumns(ITable typeSchema, Column[] dbColumns) + { + var newColumns = new List(); + foreach (var typeColumn in typeSchema.Columns) + { + if (!dbColumns.ToList().Exists(c => c.Name.Trim('[', ']') == typeColumn.Name)) + { + newColumns.Add(ConvertToMigratorColumn(typeColumn)); + } + } + + return newColumns; + } + + public virtual Column ConvertToMigratorColumn(SubSonic.Schema.IColumn subsonicColumns) + { + var migColumn = new Column(subsonicColumns.Name, subsonicColumns.DataType); + + if (subsonicColumns.IsPrimaryKey) + { + migColumn.ColumnProperty = ColumnProperty.PrimaryKey; + } + + if (subsonicColumns.IsNullable) + { + migColumn.ColumnProperty = migColumn.ColumnProperty | ColumnProperty.Null; + } + else + { + migColumn.ColumnProperty = migColumn.ColumnProperty | ColumnProperty.NotNull; + migColumn.DefaultValue = false; + } + + return migColumn; + } + } +} diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 3b2e156cf..5d9c058b9 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -128,13 +128,16 @@ False Libraries\Exceptioneer.WindowsFormsClient.dll - + + False ..\packages\MigratorDotNet.0.9.0.28138\lib\Net40\Migrator.dll - + + False ..\packages\MigratorDotNet.0.9.0.28138\lib\Net40\Migrator.Framework.dll - + + False ..\packages\MigratorDotNet.0.9.0.28138\lib\Net40\Migrator.Providers.dll @@ -162,6 +165,10 @@ + + + + @@ -269,7 +276,6 @@ -