diff --git a/src/Orchard.Tests/Data/Migrations/DatabaseMigrationManagerTests.cs b/src/Orchard.Tests/Data/Migrations/DatabaseMigrationManagerTests.cs new file mode 100644 index 000000000..25c7039dd --- /dev/null +++ b/src/Orchard.Tests/Data/Migrations/DatabaseMigrationManagerTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Data.SqlClient; +using System.IO; +using System.Threading; +using NUnit.Framework; +using Orchard.Data.Migrations; +using Orchard.Environment; +using Orchard.Tests.Records; + +namespace Orchard.Tests.Data.Migrations { + [TestFixture] + public class DatabaseMigrationManagerTests { + private string _tempDataFolder; + + [SetUp] + public void Init() { + var tempFilePath = Path.GetTempFileName(); + File.Delete(tempFilePath); + Directory.CreateDirectory(tempFilePath); + _tempDataFolder = tempFilePath; + } + + [TearDown] + public void Term() { + try { Directory.Delete(_tempDataFolder, true); } + catch (IOException) { } + } + + private static void CreateSqlServerDatabase(string databasePath) { + var databaseName = Path.GetFileNameWithoutExtension(databasePath); + using (var connection = new SqlConnection( + "Data Source=.\\SQLEXPRESS;Initial Catalog=tempdb;Integrated Security=true;User Instance=True;")) { + connection.Open(); + using (var command = connection.CreateCommand()) { + command.CommandText = + "CREATE DATABASE " + databaseName + + " ON PRIMARY (NAME=" + databaseName + + ", FILENAME='" + databasePath.Replace("'", "''") + "')"; + command.ExecuteNonQuery(); + + command.CommandText = + "EXEC sp_detach_db '" + databaseName + "', 'true'"; + command.ExecuteNonQuery(); + } + } + } + + [Test] + public void MigrationManagerShouldCreateEmptySQLiteDatabaseAtGivenLocation() { + var manager = (IDatabaseMigrationManager)new DatabaseMigrationManager(); + var coordinator = manager.CreateCoordinator("SQLite", _tempDataFolder, ""); + + coordinator.CreateDatabase(); + + Assert.That(File.Exists(Path.Combine(_tempDataFolder, "Orchard.db")), Is.True); + } + + [Test, ExpectedException(typeof(NotImplementedException))] + public void MigrationManagerShouldNotImplementTheCreationOfSqlServer() { + var manager = (IDatabaseMigrationManager)new DatabaseMigrationManager(); + var coordinator = manager.CreateCoordinator("SqlServer", _tempDataFolder, ""); + + coordinator.CreateDatabase(); + + } + + [Test] + public void CanConnectShouldBeFalseWhenSqlServerIsInvalid() { + var manager = (IDatabaseMigrationManager)new DatabaseMigrationManager(); + var coordinator = manager.CreateCoordinator("SqlServer", _tempDataFolder, "Data Source=.\\SQLEXPRESS;Initial Catalog=Hello"); + Assert.That(coordinator.CanConnect(), Is.False); + } + + [Test] + public void CanConnectShouldBeTrueWhenValidSqlServerMdfIsTargetted() { + var databasePath = Path.Combine(_tempDataFolder, "Orchard.mdf"); + CreateSqlServerDatabase(databasePath); + + var manager = (IDatabaseMigrationManager)new DatabaseMigrationManager(); + var coordinator = manager.CreateCoordinator("SqlServer", _tempDataFolder, "Data Source=.\\SQLEXPRESS;AttachDbFileName=" + databasePath + ";Integrated Security=True;User Instance=True;"); + Assert.That(coordinator.CanConnect(), Is.True); + } + + [Test] + public void SQLiteSchemaShouldBeGeneratedAndUsable() { + var manager = (IDatabaseMigrationManager) new DatabaseMigrationManager(); + var coordinator = manager.CreateCoordinator("SQLite", _tempDataFolder, ""); + + var recordDescriptors = new[] { + new RecordDescriptor {Prefix = "Hello", Type = typeof (Foo)} + }; + + coordinator.UpdateSchema(recordDescriptors); + + var sessionFactory = coordinator.BuildSessionFactory(recordDescriptors); + + var session = sessionFactory.OpenSession(); + var foo = new Foo {Name = "hi there"}; + session.Save(foo); + session.Flush(); + session.Close(); + + Assert.That(foo, Is.Not.EqualTo(0)); + + sessionFactory.Close(); + + } + + [Test] + public void SqlServerSchemaShouldBeGeneratedAndUsable() { + var databasePath = Path.Combine(_tempDataFolder, "Orchard.mdf"); + CreateSqlServerDatabase(databasePath); + + var manager = (IDatabaseMigrationManager)new DatabaseMigrationManager(); + var coordinator = manager.CreateCoordinator("SqlServer", _tempDataFolder, "Data Source=.\\SQLEXPRESS;AttachDbFileName=" + databasePath + ";Integrated Security=True;User Instance=True;"); + + var recordDescriptors = new[] { + new RecordDescriptor {Prefix = "Hello", Type = typeof (Foo)} + }; + + coordinator.UpdateSchema(recordDescriptors); + + var sessionFactory = coordinator.BuildSessionFactory(recordDescriptors); + + var session = sessionFactory.OpenSession(); + var foo = new Foo { Name = "hi there" }; + session.Save(foo); + session.Flush(); + session.Close(); + + Assert.That(foo, Is.Not.EqualTo(0)); + + sessionFactory.Close(); + } + } +} diff --git a/src/Orchard.Tests/Orchard.Tests.csproj b/src/Orchard.Tests/Orchard.Tests.csproj index bb5c16483..fddacf379 100644 --- a/src/Orchard.Tests/Orchard.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Tests.csproj @@ -135,6 +135,7 @@ Code + diff --git a/src/Orchard/Data/Migrations/DatabaseCoordinatorBase.cs b/src/Orchard/Data/Migrations/DatabaseCoordinatorBase.cs new file mode 100644 index 000000000..0d1d3bf66 --- /dev/null +++ b/src/Orchard/Data/Migrations/DatabaseCoordinatorBase.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentNHibernate.Automapping; +using FluentNHibernate.Automapping.Alterations; +using FluentNHibernate.Cfg; +using FluentNHibernate.Cfg.Db; +using FluentNHibernate.Conventions.Helpers; +using NHibernate; +using NHibernate.Tool.hbm2ddl; +using Orchard.ContentManagement.Records; +using Orchard.Data.Conventions; +using Orchard.Environment; + +namespace Orchard.Data.Migrations { + public abstract class DatabaseCoordinatorBase : IDatabaseCoordinator { + protected abstract IPersistenceConfigurer GetPersistenceConfigurer(); + + public virtual bool CanConnect() { + try { + var sessionFactory = Fluently.Configure() + .Database(GetPersistenceConfigurer()) + .BuildSessionFactory(); + try { + // attempting to open a session validates a connection can be made + var session = sessionFactory.OpenSession(); + session.Close(); + } + finally { + sessionFactory.Close(); + } + return true; + } + catch { + return false; + } + } + + public virtual void CreateDatabase() { + // creating a session factory appears to be sufficient for causing a database file to be created for inplace providers + var sessionFactory = Fluently.Configure() + .Database(GetPersistenceConfigurer()) + .BuildSessionFactory(); + sessionFactory.Close(); + } + + public void UpdateSchema(IEnumerable recordDescriptors) { + var configuration = Fluently.Configure() + .Database(GetPersistenceConfigurer()) + .Mappings(m => m.AutoMappings.Add(CreatePersistenceModel(recordDescriptors))) + .BuildConfiguration(); + + var updater = new SchemaUpdate(configuration); + updater.Execute(true /*script*/, true /*doUpdate*/); + } + + public ISessionFactory BuildSessionFactory(IEnumerable recordDescriptors) { + return Fluently.Configure() + .Database(GetPersistenceConfigurer()) + .Mappings(m => m.AutoMappings.Add(CreatePersistenceModel(recordDescriptors))) + .BuildSessionFactory(); + } + + static AutoPersistenceModel CreatePersistenceModel(IEnumerable recordDescriptors) { + return AutoMap.Source(new TypeSource(recordDescriptors)) + // Ensure that namespaces of types are never auto-imported, so that + // identical type names from different namespaces can be mapped without ambiguity + .Conventions.Setup(x => x.Add(AutoImport.Never())) + .Conventions.Add(new RecordTableNameConvention(recordDescriptors)) + .Alterations(alt => { + foreach (var recordAssembly in recordDescriptors.Select(x => x.Type.Assembly).Distinct()) { + alt.Add(new AutoMappingOverrideAlteration(recordAssembly)); + } + alt.AddFromAssemblyOf(); + alt.Add(new ContentItemAlteration(recordDescriptors)); + }) + .Conventions.AddFromAssemblyOf(); + } + + class TypeSource : ITypeSource { + private readonly IEnumerable _recordDescriptors; + + public TypeSource(IEnumerable recordDescriptors) { _recordDescriptors = recordDescriptors; } + + public IEnumerable GetTypes() { return _recordDescriptors.Select(descriptor => descriptor.Type); } + } + } +} \ No newline at end of file diff --git a/src/Orchard/Data/Migrations/DatabaseMigrationManager.cs b/src/Orchard/Data/Migrations/DatabaseMigrationManager.cs new file mode 100644 index 000000000..bde875b95 --- /dev/null +++ b/src/Orchard/Data/Migrations/DatabaseMigrationManager.cs @@ -0,0 +1,11 @@ +using System; + +namespace Orchard.Data.Migrations { + public class DatabaseMigrationManager : IDatabaseMigrationManager { + public IDatabaseCoordinator CreateCoordinator(string provider, string dataFolder, string connectionString) { + if (string.Equals(provider, "SQLite", StringComparison.InvariantCultureIgnoreCase)) + return new SQLiteDatabaseCoordinator(dataFolder, connectionString); + return new SqlServerDatabaseCoordinator(dataFolder, connectionString); + } + } +} diff --git a/src/Orchard/Data/Migrations/IDatabaseCoordinator.cs b/src/Orchard/Data/Migrations/IDatabaseCoordinator.cs new file mode 100644 index 000000000..38239f75f --- /dev/null +++ b/src/Orchard/Data/Migrations/IDatabaseCoordinator.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using NHibernate; +using Orchard.Environment; + +namespace Orchard.Data.Migrations { + public interface IDatabaseCoordinator { + bool CanConnect(); + void CreateDatabase(); + + /// + /// Should only be called in a development or evaluation environment. Automatic schema migration + /// not a really safe practice on production data sources. + /// + /// Set of known records to be applied + void UpdateSchema(IEnumerable recordDescriptors); + + ISessionFactory BuildSessionFactory(IEnumerable recordDescriptors); + } +} diff --git a/src/Orchard/Data/Migrations/IDatabaseMigrationManager.cs b/src/Orchard/Data/Migrations/IDatabaseMigrationManager.cs new file mode 100644 index 000000000..921d7e1fb --- /dev/null +++ b/src/Orchard/Data/Migrations/IDatabaseMigrationManager.cs @@ -0,0 +1,5 @@ +namespace Orchard.Data.Migrations { + public interface IDatabaseMigrationManager { + IDatabaseCoordinator CreateCoordinator(string provider, string dataFolder, string connectionString); + } +} diff --git a/src/Orchard/Data/Migrations/SQLiteDatabaseCoordinator.cs b/src/Orchard/Data/Migrations/SQLiteDatabaseCoordinator.cs new file mode 100644 index 000000000..2c90f231c --- /dev/null +++ b/src/Orchard/Data/Migrations/SQLiteDatabaseCoordinator.cs @@ -0,0 +1,26 @@ +using System.IO; +using FluentNHibernate.Cfg.Db; + +namespace Orchard.Data.Migrations { + public class SQLiteDatabaseCoordinator : DatabaseCoordinatorBase { + private readonly string _dataFolder; + private readonly string _connectionString; + + public SQLiteDatabaseCoordinator(string dataFolder, string connectionString) { + _dataFolder = dataFolder; + _connectionString = connectionString; + } + + protected override IPersistenceConfigurer GetPersistenceConfigurer() { + var persistence = SQLiteConfiguration.Standard; + if (string.IsNullOrEmpty(_connectionString)) { + persistence = persistence.UsingFile(Path.Combine(_dataFolder, "Orchard.db")); + } + else { + persistence = persistence.ConnectionString(_connectionString); + } + return persistence; + } + + } +} \ No newline at end of file diff --git a/src/Orchard/Data/Migrations/SqlServerDatabaseCoordinator.cs b/src/Orchard/Data/Migrations/SqlServerDatabaseCoordinator.cs new file mode 100644 index 000000000..0df8cb0bd --- /dev/null +++ b/src/Orchard/Data/Migrations/SqlServerDatabaseCoordinator.cs @@ -0,0 +1,26 @@ +using System; +using FluentNHibernate.Cfg.Db; + +namespace Orchard.Data.Migrations { + public class SqlServerDatabaseCoordinator : DatabaseCoordinatorBase { + private readonly string _dataFolder; + private readonly string _connectionString; + + public SqlServerDatabaseCoordinator(string dataFolder, string connectionString) { + _dataFolder = dataFolder; + _connectionString = connectionString; + } + + + protected override IPersistenceConfigurer GetPersistenceConfigurer() { + var persistence = MsSqlConfiguration.MsSql2008; + if (string.IsNullOrEmpty(_connectionString)) { + throw new NotImplementedException(); + } + else { + persistence = persistence.ConnectionString(_connectionString); + } + return persistence; + } + } +} \ No newline at end of file diff --git a/src/Orchard/Orchard.csproj b/src/Orchard/Orchard.csproj index 5a29de59b..f5d0b0b99 100644 --- a/src/Orchard/Orchard.csproj +++ b/src/Orchard/Orchard.csproj @@ -133,6 +133,12 @@ + + + + + +