diff --git a/.github/workflows/sql/oracle.sql b/.github/workflows/sql/oracle.sql index 5dc26fc6..6423ae4c 100644 --- a/.github/workflows/sql/oracle.sql +++ b/.github/workflows/sql/oracle.sql @@ -14,6 +14,7 @@ grant resource to k with admin option; grant connect to k with admin option; grant unlimited tablespace to k with admin option; grant select on v_$session to k with grant option; +grant select on sys.gv_$session to k with grant option; grant alter system to k; exit; \ No newline at end of file diff --git a/src/Migrator.Tests/Database/DatabaseName/DatabaseNameService.cs b/src/Migrator.Tests/Database/DatabaseName/DatabaseNameService.cs index 8d8e7d2f..505e396c 100644 --- a/src/Migrator.Tests/Database/DatabaseName/DatabaseNameService.cs +++ b/src/Migrator.Tests/Database/DatabaseName/DatabaseNameService.cs @@ -1,16 +1,15 @@ using System; using System.Globalization; using System.IO; -using System.Linq; +using System.Security.Cryptography; using System.Text.RegularExpressions; using Migrator.Tests.Database.DatabaseName.Interfaces; -using Migrator.Tests.Database.GuidServices.Interfaces; namespace Migrator.Test.Shared.Database; -public partial class DatabaseNameService(TimeProvider timeProvider, IGuidService guidService) : IDatabaseNameService +public partial class DatabaseNameService(TimeProvider timeProvider) : IDatabaseNameService { - private const string TestDatabaseString = "Test"; + private const string TestDatabaseString = "T"; private const string TimeStampPattern = "yyyyMMddHHmmssfff"; public DateTime? ReadTimeStampFromString(string name) @@ -33,14 +32,25 @@ public string CreateDatabaseName() var dateTimePattern = timeProvider.GetUtcNow() .ToString(TimeStampPattern); - var randomString = string.Concat(guidService.NewGuid() - .ToString("N") - .Reverse() - .Take(9)); + var randomString = CreateRandomChars(7); return $"{dateTimePattern}{TestDatabaseString}{randomString}"; } - [GeneratedRegex(@"^(\d+)(?=Test.{9}$)")] + private string CreateRandomChars(int length) + { + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var stringChars = new char[length]; + + for (var i = 0; i < length; i++) + { + var index = RandomNumberGenerator.GetInt32(chars.Length); + stringChars[i] = chars[index]; + } + + return new string(stringChars); + } + + [GeneratedRegex(@"^([\d]+)(?=T.{7}$)")] private static partial Regex DateTimeRegex(); -} \ No newline at end of file +} diff --git a/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs b/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs index 9b5d1fee..6d41ace4 100644 --- a/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs +++ b/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs @@ -1,7 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text.RegularExpressions; +using System.Text; using System.Threading; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework.Data.Common; @@ -19,17 +18,21 @@ namespace Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices; + +/// +/// We use the tablespace users since the server container is recreated before the test runs (once per github workflow run) +/// +/// +/// public class OracleDatabaseIntegrationTestService( TimeProvider timeProvider, IDatabaseNameService databaseNameService) : DatabaseIntegrationTestServiceBase(databaseNameService), IDatabaseIntegrationTestService { - private const string TableSpacePrefix = "TS_"; private const string UserStringKey = "User Id"; private const string PasswordStringKey = "Password"; private const string ReplaceString = "RandomStringThatIsNotQuotedByTheBuilderDoNotChange"; private readonly MappingSchema _mappingSchema = new MappingSchemaFactory().CreateOracleMappingSchema(); - private Regex _tablespaceRegex = new("^TS_TESTS_"); /// /// Creates an oracle database for test purposes. @@ -61,8 +64,6 @@ public class OracleDatabaseIntegrationTestService( /// public override async Task CreateTestDatabaseAsync(DatabaseConnectionConfig databaseConnectionConfig, CancellationToken cancellationToken) { - DataConnection context; - var tempDatabaseConnectionConfig = databaseConnectionConfig.Adapt(); var connectionStringBuilder = new OracleConnectionStringBuilder() @@ -82,15 +83,12 @@ public override async Task CreateTestDatabaseAsync(DatabaseConnect var tempUserName = DatabaseNameService.CreateDatabaseName(); - List userNames; - var dataOptions = new DataOptions().UseOracle(databaseConnectionConfig.ConnectionString) .UseMappingSchema(_mappingSchema); - using (context = new DataConnection(dataOptions)) - { - userNames = await context.QueryToListAsync("SELECT username FROM all_users", cancellationToken); - } + using var context = new DataConnection(dataOptions); + + var userNames = await context.GetTable().Select(x => x.UserName).ToListAsync(cancellationToken); var toBeDeletedUsers = userNames.Where(x => { @@ -112,49 +110,33 @@ await Parallel.ForEachAsync( }; await DropDatabaseAsync(databaseInfoToBeDeleted, cancellationTokenInner); - }); - using (context = new DataConnection(dataOptions)) - { - // To be on the safe side we check for table spaces used in tests that have not been deleted for any reason (possible connection issues/concurrent deletion attempts - there is - // no transaction for DDL in Oracle etc.). - var tableSpaceNames = await context.GetTable() - .Select(x => x.TablespaceName) - .ToListAsync(cancellationToken); - - var toBeDeletedTableSpaces = tableSpaceNames - .Where(x => - { - var replacedTablespaceString = _tablespaceRegex.Replace(x, ""); - var creationDate = DatabaseNameService.ReadTimeStampFromString(replacedTablespaceString); - return creationDate.HasValue && creationDate.Value < timeProvider.GetUtcNow().Subtract(_MinTimeSpanBeforeDatabaseDeletion); - }); + var stringBuilder = new StringBuilder(); + stringBuilder.Append($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\""); + stringBuilder.AppendLine($"DEFAULT TABLESPACE users"); + stringBuilder.AppendLine($"TEMPORARY TABLESPACE TEMP"); + stringBuilder.AppendLine($"QUOTA UNLIMITED ON users"); - foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces) - { - await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken); - } + await context.ExecuteAsync(stringBuilder.ToString(), cancellationToken); - await context.ExecuteAsync($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"", cancellationToken); + var privileges = new[] + { + "CONNECT", + "CREATE SESSION", + "RESOURCE", + "UNLIMITED TABLESPACE" + }; - var privileges = new[] - { - "CONNECT", - "CREATE SESSION", - "RESOURCE", - "UNLIMITED TABLESPACE" - }; - - await context.ExecuteAsync($"GRANT {string.Join(", ", privileges)} TO \"{tempUserName}\"", cancellationToken); - await context.ExecuteAsync($"GRANT SELECT ON SYS.V_$SESSION TO \"{tempUserName}\"", cancellationToken); - } + await context.ExecuteAsync($"GRANT {string.Join(", ", privileges)} TO \"{tempUserName}\"", cancellationToken); + await context.ExecuteAsync($"GRANT SELECT ON SYS.GV_$SESSION TO \"{tempUserName}\"", cancellationToken); connectionStringBuilder.Add(UserStringKey, ReplaceString); connectionStringBuilder.Add(PasswordStringKey, ReplaceString); tempDatabaseConnectionConfig.ConnectionString = connectionStringBuilder.ConnectionString; tempDatabaseConnectionConfig.ConnectionString = tempDatabaseConnectionConfig.ConnectionString.Replace(ReplaceString, $"\"{tempUserName}\""); + tempDatabaseConnectionConfig.Schema = tempUserName; var databaseInfo = new DatabaseInfo { @@ -168,16 +150,18 @@ await Parallel.ForEachAsync( public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(databaseInfo); + var creationDate = ReadTimeStampFromDatabaseName(databaseInfo.SchemaName); var dataOptions = new DataOptions().UseOracle(databaseInfo.DatabaseConnectionConfigMaster.ConnectionString) .UseMappingSchema(_mappingSchema); + using var context = new DataConnection(dataOptions); + var maxAttempts = 4; var delayBetweenAttempts = TimeSpan.FromSeconds(1); - using var context = new DataConnection(dataOptions); - for (var i = 0; i < maxAttempts; i++) { try @@ -192,6 +176,13 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella await context.ExecuteAsync(killStatement, cancellationToken); } + var userExists = context.GetTable().Any(x => x.UserName == databaseInfo.SchemaName); + + if (!userExists) + { + break; + } + await context.ExecuteAsync($"DROP USER \"{databaseInfo.SchemaName}\" CASCADE", cancellationToken); } catch @@ -207,19 +198,13 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella { break; } - } - await Task.Delay(delayBetweenAttempts, cancellationToken); + await Task.Delay(delayBetweenAttempts, cancellationToken); - delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1)); + delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1)); + } } - var tablespaceName = $"{TableSpacePrefix}{databaseInfo.SchemaName}"; - - var tablespaces = await context.GetTable().ToListAsync(cancellationToken); - - await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken); - await context.ExecuteAsync($"PURGE RECYCLEBIN", cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs index dfd236c2..f12b7f27 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs +++ b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs @@ -64,10 +64,9 @@ protected void DropTestTables() } } - protected async Task BeginOracleTransactionAsync() { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var configReader = new ConfigurationReader(); var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.OracleId); diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs index 97c315f9..0c78c68b 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddTableTestsBase.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Data; +using System.Linq; using DotNetProjects.Migrator.Framework; using Migrator.Tests.Providers.Base; using NUnit.Framework; @@ -30,6 +32,86 @@ public void AddTable_PrimaryKeyWithIdentity_Success() Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); } + [Test] + public void AddTable_PrimaryKeyAndIdentity_Success() + { + // Arrange + var tableName = "TableName"; + var column1Name = "Column1"; + var column2Name = "Column2"; + + // Act + Provider.AddTable(tableName, + new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKey | ColumnProperty.Identity), + new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) + ); + + // Assert + var column1 = Provider.GetColumnByName(tableName, column1Name); + var column2 = Provider.GetColumnByName(tableName, column2Name); + + Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); + Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + } + + [Test] + public void AddTable_PrimaryKeyAndIdentityWithInsertNull_Success() + { + // Arrange + var tableName = "TableName"; + var column1Name = "Column1"; + var column2Name = "Column2"; + + // Act + Provider.AddTable(tableName, + new Column(column1Name, DbType.Int32, ColumnProperty.NotNull | ColumnProperty.PrimaryKey | ColumnProperty.Identity), + new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) + ); + + Provider.Insert(table: tableName, [column2Name], [999]); + + // Assert + var column1 = Provider.GetColumnByName(tableName, column1Name); + var column2 = Provider.GetColumnByName(tableName, column2Name); + + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd: cmd, table: tableName, columns: [column1Name, column2Name]); + + List<(int, int)> records = []; + + while (reader.Read()) + { + records.Add((reader.GetInt32(0), reader.GetInt32(1))); + } + + Assert.That(records.Single().Item1, Is.EqualTo(1)); + + Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); + Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + } + + [Test] + public void AddTable_PrimaryKeyAndIdentityWithoutNotNull_Success() + { + // Arrange + var tableName = "TableName"; + var column1Name = "Column1"; + var column2Name = "Column2"; + + // Act + Provider.AddTable(tableName, + new Column(column1Name, DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.Identity), + new Column(column2Name, DbType.Int32, ColumnProperty.NotNull) + ); + + // Assert + var column1 = Provider.GetColumnByName(tableName, column1Name); + var column2 = Provider.GetColumnByName(tableName, column2Name); + + Assert.That(column1.ColumnProperty.HasFlag(ColumnProperty.PrimaryKeyWithIdentity), Is.True); + Assert.That(column2.ColumnProperty.HasFlag(ColumnProperty.NotNull), Is.True); + } + [Test] public void AddTable_NotNull_Success() { diff --git a/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs new file mode 100644 index 00000000..58d525bb --- /dev/null +++ b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs @@ -0,0 +1,50 @@ +using System.Data; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.Generic; + +public abstract class Generic_DefaultValueTestsBase : TransformationProviderBase +{ + [Test] + public void DefaultValue_Null_Success() + { + const string tableNameSource = "SourceTable"; + const string columnName1Target = "TargetColumn1"; + + Provider.AddTable(tableNameSource, + new Column(columnName1Target, DbType.Int32, ColumnProperty.Null, null) + ); + + Provider.ChangeColumn(tableNameSource, new Column(columnName1Target, DbType.Int32, ColumnProperty.NotNull)); + } + + [Test] + public void DefaultValue_ConvertStringToNotNull_DoesNotThrow() + { + const string tableNameSource = "SourceTable"; + const string columnName1Target = "TargetColumn1"; + + Provider.AddTable(tableNameSource, + new Column(columnName1Target, DbType.String, 32, ColumnProperty.NotNull) + ); + + Provider.ChangeColumn(tableNameSource, new Column(columnName1Target, DbType.String, ColumnProperty.Null)); + } + + [Test] + public void RemoveColumnDefaultValue_DoesNotThrow() + { + const string tableNameSource = "TableName"; + const string columnName1 = "ColumnName1"; + + Provider.AddTable(tableNameSource, + new Column(columnName1, DbType.Int32, ColumnProperty.NotNull, 10) + ); + + Provider.RemoveColumnDefaultValue(tableNameSource, columnName1); + + Provider.ChangeColumn(tableNameSource, new Column(columnName1, DbType.Int32, ColumnProperty.Null)); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_DefaultValueTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_DefaultValueTests.cs new file mode 100644 index 00000000..28097140 --- /dev/null +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_DefaultValueTests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.OracleProvider; + +[TestFixture] +[Category("Oracle")] +public class OracleTransformationProvider_DefaultValueTests : Generic_DefaultValueTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginOracleTransactionAsync(); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgresSQLTransformationProvider_AddPrimaryKeyTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddPrimaryKeyTests.cs similarity index 100% rename from src/Migrator.Tests/Providers/PostgreSQL/PostgresSQLTransformationProvider_AddPrimaryKeyTests.cs rename to src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddPrimaryKeyTests.cs diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgresSQLTransformationProvider_ChangeColumnTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs similarity index 83% rename from src/Migrator.Tests/Providers/PostgreSQL/PostgresSQLTransformationProvider_ChangeColumnTests.cs rename to src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs index 746ba161..cb91de8e 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgresSQLTransformationProvider_ChangeColumnTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ChangeColumnTests.cs @@ -1,4 +1,6 @@ +using System.Data; using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; using Migrator.Tests.Providers.Generic; using NUnit.Framework; diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_DefaultValueTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_DefaultValueTests.cs new file mode 100644 index 00000000..a985d3ab --- /dev/null +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_DefaultValueTests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.PostgreSQL; + +[TestFixture] +[Category("Postgre")] +public class PostgreSQLTransformationProvider_DefaultValueTests : Generic_DefaultValueTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginPostgreSQLTransactionAsync(); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_DefaultValueTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_DefaultValueTests.cs new file mode 100644 index 00000000..73caec5d --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_DefaultValueTests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLServer; + +[TestFixture] +[Category("SqlServer")] +public class SQLServerTransformationProvider_DefaultValueTests : Generic_DefaultValueTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLServerTransactionAsync(); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_DefaultValueTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_DefaultValueTests.cs new file mode 100644 index 00000000..e6bac437 --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_DefaultValueTests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLite; + +[TestFixture] +[Category("SQLite")] +public class SQLiteTransformationProvider_DefaultValueTests : Generic_AddIndexTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLiteTransactionAsync(); + } +} diff --git a/src/Migrator.Tests/Settings/Models/DatabaseConnectionConfig.cs b/src/Migrator.Tests/Settings/Models/DatabaseConnectionConfig.cs index e7d96884..e41c7ea8 100644 --- a/src/Migrator.Tests/Settings/Models/DatabaseConnectionConfig.cs +++ b/src/Migrator.Tests/Settings/Models/DatabaseConnectionConfig.cs @@ -2,10 +2,18 @@ namespace Migrator.Tests.Settings.Models; public class DatabaseConnectionConfig { + /// + /// Gets or sets the connection string. + /// public string ConnectionString { get; set; } /// /// Gets or sets the connection identifier. /// public string Id { get; set; } + + /// + /// Gets or sets the schema name. + /// + public string Schema { get; set; } } \ No newline at end of file diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index 4c48d311..6a5a35ed 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -635,7 +635,11 @@ public override Column[] GetColumns(string table) } // dataDefaultString contains ISEQ$$ if the column is an identity column - if (!string.IsNullOrWhiteSpace(dataDefaultString) && !dataDefaultString.Contains("ISEQ$$") && !dataDefaultString.Contains(".nextval")) + if ( + !string.IsNullOrWhiteSpace(dataDefaultString) && + (column.Type == DbType.String || !dataDefaultString.Equals("null", StringComparison.OrdinalIgnoreCase)) && + !dataDefaultString.Contains("ISEQ$$") && + !dataDefaultString.Contains(".nextval")) { // This is only necessary because older versions of this migrator added single quotes for numerics. var singleQuoteStrippedString = dataDefaultString.Replace("'", "");