diff --git a/src/Simple.Migrations.IntegrationTests/ConnectionStrings.cs b/src/Simple.Migrations.IntegrationTests/ConnectionStrings.cs index 46e8edd..9189e81 100644 --- a/src/Simple.Migrations.IntegrationTests/ConnectionStrings.cs +++ b/src/Simple.Migrations.IntegrationTests/ConnectionStrings.cs @@ -13,5 +13,6 @@ public static class ConnectionStrings public static readonly string MSSQL = @"Server=.;Database=SimpleMigratorTests;Trusted_Connection=True;"; public static readonly string MySQL = @"Server=localhost;Database=SimpleMigrator;Uid=SimpleMigrator;Pwd=SimpleMigrator;"; public static readonly string PostgreSQL = @"Server=localhost;Port=5432;Database=SimpleMigrator;User ID=SimpleMigrator;Password=SimpleMigrator"; + public static readonly string Oracle = @"Data Source=localhost:32769/ORCLPDB1.localdomain;User ID=SimpleMigrator;Password=SimpleMigrator"; } } diff --git a/src/Simple.Migrations.IntegrationTests/Oracle/OracleStringsProvider.cs b/src/Simple.Migrations.IntegrationTests/Oracle/OracleStringsProvider.cs new file mode 100644 index 0000000..7cb5d5e --- /dev/null +++ b/src/Simple.Migrations.IntegrationTests/Oracle/OracleStringsProvider.cs @@ -0,0 +1,11 @@ +namespace Simple.Migrations.IntegrationTests.Oracle +{ + public class OracleStringsProvider : IMigrationStringsProvider + { + public string CreateUsersTableUp => @"CREATE TABLE Users ( + Id INTEGER NOT NULL PRIMARY KEY, + Name VARCHAR2(100) NOT NULL + )"; + public string CreateUsersTableDown => "DROP TABLE Users PURGE"; + } +} \ No newline at end of file diff --git a/src/Simple.Migrations.IntegrationTests/Oracle/OracleTests.cs b/src/Simple.Migrations.IntegrationTests/Oracle/OracleTests.cs new file mode 100644 index 0000000..d848f8d --- /dev/null +++ b/src/Simple.Migrations.IntegrationTests/Oracle/OracleTests.cs @@ -0,0 +1,59 @@ +using System.Data.Common; +using Npgsql; +using NUnit.Framework; +using Oracle.ManagedDataAccess.Client; +using Simple.Migrations.IntegrationTests.Postgresql; +using SimpleMigrations; +using SimpleMigrations.DatabaseProvider; + +namespace Simple.Migrations.IntegrationTests.Oracle +{ + [TestFixture] + public class OracleTests : TestsBase + { + protected override IDatabaseProvider CreateDatabaseProvider() => new OracleDatabaseProvider(this.CreateConnection); + + protected override IMigrationStringsProvider MigrationStringsProvider { get; } = new OracleStringsProvider(); + + protected override bool SupportConcurrentMigrators => true; + + protected override void Clean() + { + var connection = this.CreateOraConnection(); + connection.Open(); + + using (var cmd = new OracleCommand(@" +BEGIN + FOR CURTAB IN (SELECT TABLE_NAME FROM USER_TABLES) LOOP + BEGIN + EXECUTE IMMEDIATE 'DROP TABLE '||curtab.table_name||' PURGE'; + EXCEPTION WHEN OTHERS THEN + IF (SQLCODE = -942) THEN + NULL; + ELSE + RAISE; + END IF; + END; + END LOOP; + + BEGIN + EXECUTE IMMEDIATE 'DROP SEQUENCE SEQ_VERSION_TABLE'; + EXCEPTION WHEN OTHERS THEN + IF (SQLCODE = -2289) THEN + NULL; + ELSE + RAISE; + END IF; + END; +END; +", connection)) + { + cmd.ExecuteNonQuery(); + } + } + + private OracleConnection CreateOraConnection() => new OracleConnection(ConnectionStrings.Oracle); + + protected override DbConnection CreateConnection() => this.CreateOraConnection(); + } +} diff --git a/src/Simple.Migrations.IntegrationTests/Simple.Migrations.IntegrationTests.csproj b/src/Simple.Migrations.IntegrationTests/Simple.Migrations.IntegrationTests.csproj index 9ecba21..064acd7 100644 --- a/src/Simple.Migrations.IntegrationTests/Simple.Migrations.IntegrationTests.csproj +++ b/src/Simple.Migrations.IntegrationTests/Simple.Migrations.IntegrationTests.csproj @@ -7,12 +7,18 @@ true + + full + true + + + @@ -32,4 +38,8 @@ + + + + diff --git a/src/Simple.Migrations/DatabaseProvider/OracleDatabaseProvider.cs b/src/Simple.Migrations/DatabaseProvider/OracleDatabaseProvider.cs new file mode 100644 index 0000000..1a0d7f1 --- /dev/null +++ b/src/Simple.Migrations/DatabaseProvider/OracleDatabaseProvider.cs @@ -0,0 +1,121 @@ +using System; +using System.Data.Common; + +namespace SimpleMigrations.DatabaseProvider +{ + /// + /// Class which can read from / write to a version table in an PostgreSQL database + /// + /// + /// PostgreSQL supports advisory locks, so these are used to guard against concurrent migrators. + /// + public class OracleDatabaseProvider : DatabaseProviderBaseWithVersionTableLock + { + /// + /// Name of Oracle Sequence for generating Id in version table + /// + /// + public string VersionSequenceName { get; set; } = "SEQ_VERSION_TABLE"; + + /// + /// Controls whether or not to try and create the schema if it does not exist. + /// + /// + /// If this is set to false then no schema is created. It is the user's responsibility to create the schema + /// (if necessary) with the correct name and permissions before running the . This may be + /// required if the user which Simple.Migrator is running as does not have the correct permissions to check whether the + /// schema has been created. + /// + public bool CreateSchema { get; set; } = false; + + + /// + /// Initialises a new instance of the class + /// + /// Connection to use to run migrations. The caller is responsible for closing this. + public OracleDatabaseProvider(Func connectionFactory) + : base(connectionFactory) + { + TableName = "VERSION_INFO"; + } + + + /// + /// Returns SQL to create the version table + /// + /// SQL to create the version table + protected override string GetCreateVersionTableSql() + { + return $@"DECLARE + PROCEDURE CREATE_IF_NOT_EXISTS(p_object VARCHAR2, p_sql VARCHAR2) + IS + c_object user_objects.object_name%type; + BEGIN + BEGIN + SELECT object_name INTO c_object FROM user_objects WHERE object_name=p_object; + EXCEPTION WHEN no_data_found then + BEGIN + EXECUTE IMMEDIATE p_sql; + EXCEPTION WHEN OTHERS THEN + IF (SQLCODE = -955) THEN + NULL; + ELSE + RAISE; + END IF; + END; + END; + END; +BEGIN + CREATE_IF_NOT_EXISTS('{this.VersionSequenceName}','CREATE SEQUENCE {this.VersionSequenceName} START WITH 1 NOCACHE'); + + CREATE_IF_NOT_EXISTS('{this.TableName}','CREATE TABLE {this.TableName}( + ID INTEGER PRIMARY KEY, + VERSION INTEGER NOT NULL, + APPLIED_ON timestamp with time zone, + DESCRIPTION varchar2(4000) NOT NULL + )'); +END; +"; + } + + + + + /// + /// Returns SQL to fetch the current version from the version table + /// + /// SQL to fetch the current version from the version table + protected override string GetCurrentVersionSql() + { + return $@"SELECT VERSION FROM (SELECT VERSION FROM {this.TableName} ORDER BY ID DESC) WHERE ROWNUM=1"; + } + + /// + /// Returns SQL to update the current version in the version table + /// + /// SQL to update the current version in the version table + protected override string GetSetVersionSql() + { + //RETURNING VERSION INTO :OldVersion - required use of bind variable to aviod oracle error + return $@"INSERT INTO {this.TableName} (ID, VERSION, APPLIED_ON, DESCRIPTION) VALUES ({this.VersionSequenceName}.NEXTVAL, :Version, CURRENT_TIMESTAMP, :Description) RETURNING VERSION INTO :OldVersion"; + } + + protected override void AcquireVersionTableLock() + { + VersionTableLockTransaction = this.VersionTableConnection.BeginTransaction(); + using (var cmd = this.VersionTableConnection.CreateCommand()) + { + cmd.Transaction = VersionTableLockTransaction; + cmd.CommandText = $"LOCK TABLE {TableName} IN EXCLUSIVE MODE"; + cmd.ExecuteNonQuery(); + } + } + + protected override void ReleaseVersionTableLock() + { + VersionTableLockTransaction.Commit(); + VersionTableLockTransaction.Dispose(); + VersionTableLockTransaction = null; + } + } +}