From e2aae096eb9c86f97f014df0840083964c9b1222 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Sat, 15 Nov 2025 12:24:28 +0100 Subject: [PATCH 1/6] Oracle: Handle "NULL" as default value --- .../DatabaseName/DatabaseNameService.cs | 30 ++-- .../OracleDatabaseIntegrationTestService.cs | 159 +++++++++++++----- .../Generic/Generic_AddTableTestsBase.cs | 82 +++++++++ .../Generic/Generic_DefaultValueTestsBase.cs | 35 ++++ ...ransformationProvider_DefaultValueTests.cs | 16 ++ ...nsformationProvider_AddPrimaryKeyTests.cs} | 0 ...ansformationProvider_ChangeColumnTests.cs} | 2 + ...ransformationProvider_DefaultValueTests.cs | 16 ++ ...ransformationProvider_DefaultValueTests.cs | 16 ++ ...ransformationProvider_DefaultValueTests.cs | 16 ++ .../Models/DatabaseConnectionConfig.cs | 8 + .../Oracle/OracleTransformationProvider.cs | 2 +- 12 files changed, 325 insertions(+), 57 deletions(-) create mode 100644 src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs create mode 100644 src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_DefaultValueTests.cs rename src/Migrator.Tests/Providers/PostgreSQL/{PostgresSQLTransformationProvider_AddPrimaryKeyTests.cs => PostgreSQLTransformationProvider_AddPrimaryKeyTests.cs} (100%) rename src/Migrator.Tests/Providers/PostgreSQL/{PostgresSQLTransformationProvider_ChangeColumnTests.cs => PostgreSQLTransformationProvider_ChangeColumnTests.cs} (83%) create mode 100644 src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_DefaultValueTests.cs create mode 100644 src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_DefaultValueTests.cs create mode 100644 src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_DefaultValueTests.cs 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..2e2a79be 100644 --- a/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs +++ b/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -19,7 +19,7 @@ namespace Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices; -public class OracleDatabaseIntegrationTestService( +public partial class OracleDatabaseIntegrationTestService( TimeProvider timeProvider, IDatabaseNameService databaseNameService) : DatabaseIntegrationTestServiceBase(databaseNameService), IDatabaseIntegrationTestService @@ -61,8 +61,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 +80,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 +107,84 @@ await Parallel.ForEachAsync( }; await DropDatabaseAsync(databaseInfoToBeDeleted, cancellationTokenInner); + }); + + // 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 = TableSpacePrefixRegex().Replace(x, ""); + var creationDate = DatabaseNameService.ReadTimeStampFromString(replacedTablespaceString); + return creationDate.HasValue && creationDate.Value < timeProvider.GetUtcNow().Subtract(_MinTimeSpanBeforeDatabaseDeletion); }); - using (context = new DataConnection(dataOptions)) + foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces) { - // 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 maxAttempts = 4; + var delayBetweenAttempts = TimeSpan.FromSeconds(1); - foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces) + for (var i = 0; i < maxAttempts; i++) { - await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken); - } + try + { + await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken); + } + catch + { + var exists = await context.GetTable().AnyAsync(x => x.TablespaceName == toBeDeletedTableSpace, cancellationToken); - await context.ExecuteAsync($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\"", cancellationToken); + if (!exists) + { + break; + } - 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); + if (i + 1 == maxAttempts) + { + throw; + } + + await Task.Delay(delayBetweenAttempts, cancellationToken); + + delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1)); + } + } } + var tableSpaceName = $"{TableSpacePrefix}{tempUserName}"; + + var createTablespaceSql = $"CREATE TABLESPACE {tableSpaceName}"; + await context.ExecuteAsync(createTablespaceSql, cancellationToken: cancellationToken); + + var stringBuilder = new StringBuilder(); + stringBuilder.Append($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\""); + stringBuilder.AppendLine($"DEFAULT TABLESPACE {tableSpaceName}"); + stringBuilder.AppendLine($"TEMPORARY TABLESPACE TEMP"); + stringBuilder.AppendLine($"QUOTA UNLIMITED ON {tableSpaceName}"); + + await context.ExecuteAsync(stringBuilder.ToString(), cancellationToken); + + 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.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 +198,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 +224,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 +246,47 @@ 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); + maxAttempts = 4; + delayBetweenAttempts = TimeSpan.FromSeconds(1); + + for (var i = 0; i < maxAttempts; i++) + { + try + { + await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken); + } + catch + { + var exists = await context.GetTable().AnyAsync(x => x.TablespaceName == tablespaceName, cancellationToken); + + if (!exists) + { + break; + } - await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken); + if (i + 1 == maxAttempts) + { + throw; + } + + await Task.Delay(delayBetweenAttempts, cancellationToken); + + delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1)); + } + } await context.ExecuteAsync($"PURGE RECYCLEBIN", cancellationToken); } -} \ No newline at end of file + + [GeneratedRegex("^TS_TESTS_")] + private static partial Regex TableSpacePrefixRegex(); +} 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..3929be05 --- /dev/null +++ b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs @@ -0,0 +1,35 @@ +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)); + } +} \ 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..d1d0ba20 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -635,7 +635,7 @@ 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) && !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("'", ""); From 104b46188f1ca4cb1c3e08109849c8870ba4a152 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Sat, 15 Nov 2025 12:45:21 +0100 Subject: [PATCH 2/6] Removed tablespace creation and deletion since the server container is recreated before the test runs (once per github workflow run) --- .../OracleDatabaseIntegrationTestService.cs | 100 ++---------------- 1 file changed, 9 insertions(+), 91 deletions(-) diff --git a/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs b/src/Migrator.Tests/Database/DerivedDatabaseIntegrationTestServices/OracleDatabaseIntegrationTestService.cs index 2e2a79be..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.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework.Data.Common; @@ -19,17 +18,21 @@ namespace Migrator.Tests.Database.DerivedDatabaseIntegrationTestServices; -public partial class OracleDatabaseIntegrationTestService( + +/// +/// 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. @@ -109,62 +112,11 @@ await Parallel.ForEachAsync( await DropDatabaseAsync(databaseInfoToBeDeleted, cancellationTokenInner); }); - // 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 = TableSpacePrefixRegex().Replace(x, ""); - var creationDate = DatabaseNameService.ReadTimeStampFromString(replacedTablespaceString); - return creationDate.HasValue && creationDate.Value < timeProvider.GetUtcNow().Subtract(_MinTimeSpanBeforeDatabaseDeletion); - }); - - foreach (var toBeDeletedTableSpace in toBeDeletedTableSpaces) - { - var maxAttempts = 4; - var delayBetweenAttempts = TimeSpan.FromSeconds(1); - - for (var i = 0; i < maxAttempts; i++) - { - try - { - await context.ExecuteAsync($"DROP TABLESPACE {toBeDeletedTableSpace} INCLUDING CONTENTS AND DATAFILES", cancellationToken); - } - catch - { - var exists = await context.GetTable().AnyAsync(x => x.TablespaceName == toBeDeletedTableSpace, cancellationToken); - - if (!exists) - { - break; - } - - if (i + 1 == maxAttempts) - { - throw; - } - - await Task.Delay(delayBetweenAttempts, cancellationToken); - - delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1)); - } - } - } - - var tableSpaceName = $"{TableSpacePrefix}{tempUserName}"; - - var createTablespaceSql = $"CREATE TABLESPACE {tableSpaceName}"; - await context.ExecuteAsync(createTablespaceSql, cancellationToken: cancellationToken); - var stringBuilder = new StringBuilder(); stringBuilder.Append($"CREATE USER \"{tempUserName}\" IDENTIFIED BY \"{tempUserName}\""); - stringBuilder.AppendLine($"DEFAULT TABLESPACE {tableSpaceName}"); + stringBuilder.AppendLine($"DEFAULT TABLESPACE users"); stringBuilder.AppendLine($"TEMPORARY TABLESPACE TEMP"); - stringBuilder.AppendLine($"QUOTA UNLIMITED ON {tableSpaceName}"); + stringBuilder.AppendLine($"QUOTA UNLIMITED ON users"); await context.ExecuteAsync(stringBuilder.ToString(), cancellationToken); @@ -253,40 +205,6 @@ public override async Task DropDatabaseAsync(DatabaseInfo databaseInfo, Cancella } } - var tablespaceName = $"{TableSpacePrefix}{databaseInfo.SchemaName}"; - - maxAttempts = 4; - delayBetweenAttempts = TimeSpan.FromSeconds(1); - - for (var i = 0; i < maxAttempts; i++) - { - try - { - await context.ExecuteAsync($"DROP TABLESPACE {tablespaceName} INCLUDING CONTENTS AND DATAFILES", cancellationToken); - } - catch - { - var exists = await context.GetTable().AnyAsync(x => x.TablespaceName == tablespaceName, cancellationToken); - - if (!exists) - { - break; - } - - if (i + 1 == maxAttempts) - { - throw; - } - - await Task.Delay(delayBetweenAttempts, cancellationToken); - - delayBetweenAttempts = delayBetweenAttempts.Add(TimeSpan.FromSeconds(1)); - } - } - await context.ExecuteAsync($"PURGE RECYCLEBIN", cancellationToken); } - - [GeneratedRegex("^TS_TESTS_")] - private static partial Regex TableSpacePrefixRegex(); } From 72ea6ab2de0107e1b300094fce3effe9a2f1291e Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Sat, 15 Nov 2025 12:52:45 +0100 Subject: [PATCH 3/6] Grant privilege to k --- .github/workflows/sql/oracle.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sql/oracle.sql b/.github/workflows/sql/oracle.sql index 5dc26fc6..64623f19 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 From 5e957cf1a46ae798201c51041c532b08b0c4563a Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Sat, 15 Nov 2025 12:55:22 +0100 Subject: [PATCH 4/6] Minor change --- .github/workflows/sql/oracle.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sql/oracle.sql b/.github/workflows/sql/oracle.sql index 64623f19..6423ae4c 100644 --- a/.github/workflows/sql/oracle.sql +++ b/.github/workflows/sql/oracle.sql @@ -14,7 +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 select on sys.gv_$session to k with grant option; grant alter system to k; exit; \ No newline at end of file From 3bfe067eadc3b861e891791cd7e0aceee21294eb Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Sun, 16 Nov 2025 00:44:01 +0100 Subject: [PATCH 5/6] Remove column default value test --- .../Generic/Generic_DefaultValueTestsBase.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs index 3929be05..58d525bb 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_DefaultValueTestsBase.cs @@ -32,4 +32,19 @@ public void DefaultValue_ConvertStringToNotNull_DoesNotThrow() 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 From 8f5672152e5a7fc010afc633a5be8411a703fc37 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 17 Nov 2025 09:08:49 +0100 Subject: [PATCH 6/6] Allow default value "null" for DbType.String but not for others --- .../Providers/Base/TransformationProviderBase.cs | 3 +-- .../Providers/Impl/Oracle/OracleTransformationProvider.cs | 6 +++++- 2 files changed, 6 insertions(+), 3 deletions(-) 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/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index d1d0ba20..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.Equals("null", StringComparison.OrdinalIgnoreCase) && !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("'", "");