Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/Migrator.Tests/OracleColumnAlterationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.Data;
using DotNetProjects.Migrator.Framework;
using DotNetProjects.Migrator.Providers.Impl.Oracle;
using NUnit.Framework;

namespace Migrator.Tests;

public class OracleColumnAlterationTests
{
private sealed class RecordingProvider(Column existing)
: OracleTransformationProvider(new OracleDialect(), (IDbConnection)null, null, "default", null)
{
public List<string> Commands { get; } = [];
public override Column GetColumnByName(string table, string columnName) => existing;
public override int ExecuteNonQuery(string sql)
{
Commands.Add(sql);
return 0;
}
}

[TestCase(DbType.String, false)]
[TestCase(DbType.String, true)]
[TestCase(DbType.AnsiString, false)]
[TestCase(DbType.AnsiString, true)]
[TestCase(DbType.Binary, false)]
[TestCase(DbType.Binary, true)]
public void UnchangedLobTypeChangesNullabilityWithoutRestatingType(DbType type, bool nullable)
{
var existing = new Column("Status", type, int.MaxValue) { IsNullable = !nullable };
var requested = new Column("Status", type, int.MaxValue) { IsNullable = nullable };
using var provider = new RecordingProvider(existing);

provider.ChangeColumn("Nagel_AzureServiceBusIn", requested);

Assert.That(provider.Commands, Is.EqualTo(new[]
{
"ALTER TABLE Nagel_AzureServiceBusIn MODIFY (Status DEFAULT NULL)",
"ALTER TABLE Nagel_AzureServiceBusIn MODIFY (Status " + (nullable ? "NULL" : "NOT NULL") + ")"
}));
Assert.That(existing.IsNullable, Is.EqualTo(!nullable));
Assert.That(requested.IsNullable, Is.EqualTo(nullable));
Assert.That(requested.Size, Is.EqualTo(int.MaxValue));
}

[Test]
public void UnchangedLobWithUnchangedNullabilityOnlyRemovesDefault()
{
using var provider = new RecordingProvider(new Column("Status", DbType.String, int.MaxValue) { IsNullable = false });
provider.ChangeColumn("Example", new Column("Status", DbType.String, int.MaxValue) { IsNullable = false });
Assert.That(provider.Commands, Is.EqualTo(new[] { "ALTER TABLE Example MODIFY (Status DEFAULT NULL)" }));
}

[Test]
public void UnchangedLobAppliesDefaultSeparatelyFromNullability()
{
using var provider = new RecordingProvider(new Column("Status", DbType.String, int.MaxValue));
var requested = new Column("Status", DbType.String, int.MaxValue, "pending") { IsNullable = false };
provider.ChangeColumn("Example", requested);
Assert.That(provider.Commands, Is.EqualTo(new[]
{
"ALTER TABLE Example MODIFY (Status DEFAULT 'pending')",
"ALTER TABLE Example MODIFY (Status NOT NULL)"
}));
Assert.That(requested.DefaultValue, Is.EqualTo("pending"));
}

[Test]
public void TypeConversionAndNullabilityUseSeparateStatements()
{
using var provider = new RecordingProvider(new Column("Status", DbType.String, 80));
provider.ChangeColumn("Example", new Column("Status", DbType.String, int.MaxValue) { IsNullable = false });
Assert.That(provider.Commands, Is.EqualTo(new[]
{
"ALTER TABLE Example MODIFY (Status DEFAULT NULL)",
"ALTER TABLE Example MODIFY (Status NCLOB)",
"ALTER TABLE Example MODIFY (Status NOT NULL)"
}));
}

[Test]
public void ScalarTypeAndDefaultChangesArePreserved()
{
using var provider = new RecordingProvider(new Column("Amount", DbType.Int32));
provider.ChangeColumn("Example", new Column("Amount", DbType.Decimal)
{
Precision = 12, Scale = 4, DefaultValue = 29, IsNullable = false
});
Assert.That(provider.Commands, Is.EqualTo(new[]
{
"ALTER TABLE Example MODIFY (Amount NUMBER(12, 4) DEFAULT 29)",
"ALTER TABLE Example MODIFY (Amount NOT NULL)"
}));
}

[Test]
public void QualifiedTableAndQuotedColumnArePreserved()
{
using var provider = new RecordingProvider(new Column("select", DbType.String, int.MaxValue));
provider.ChangeColumn("audit.Example", new Column("select", DbType.String, int.MaxValue) { IsNullable = false });
Assert.That(provider.Commands, Is.EqualTo(new[]
{
"ALTER TABLE audit.Example MODIFY (\"select\" DEFAULT NULL)",
"ALTER TABLE audit.Example MODIFY (\"select\" NOT NULL)"
}));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Threading.Tasks;
using System.Data;
using System.Data.Common;
using System.Threading.Tasks;
using DotNetProjects.Migrator.Framework;
using Migrator.Tests.Providers.Generic;
using NUnit.Framework;

Expand All @@ -9,8 +12,37 @@ namespace Migrator.Tests.Providers.OracleProvider;
public class OracleTransformationProvider_ChangeColumn_Tests : Generic_ChangeColumnTestsBase
{
[SetUp]
public async Task SetUpAsync()
public async Task SetUpAsync()
{
await BeginOracleTransactionAsync();
}
}
await BeginOracleTransactionAsync();
}

[TestCase(DbType.String)]
[TestCase(DbType.AnsiString)]
[TestCase(DbType.Binary)]
public void ChangeColumn_LobNullabilityPreservesDataAndSupportsRepeatedChanges(DbType type)
{
Provider.AddTable("Test", new Column("Status", type, int.MaxValue));
object value = type == DbType.Binary ? new byte[] { 1, 2, 3 } : "pending";
Provider.Insert("Test", ["Status"], [value]);

foreach (var nullable in new[] { false, false, true, true })
{
Provider.ChangeColumn("Test", new Column("Status", type, int.MaxValue) { IsNullable = nullable });
var actual = Provider.ReadLegacyColumn("Test", "Status");
Assert.That(actual.IsNullable, Is.EqualTo(nullable));
Assert.That(actual.Type, Is.EqualTo(type));
Assert.That(actual.Size, Is.EqualTo(int.MaxValue));
if (!nullable)
{
Assert.Catch<DbException>(() => Provider.Insert("Test", ["Status"], [null]));
}
using var command = Provider.CreateCommand();
using var reader = Provider.Select(command, "Test", ["Status"]);
Assert.That(reader.Read(), Is.True);
Assert.That(reader.GetValue(0), Is.EqualTo(value));
}

Provider.Insert("Test", ["Status"], [null]);
}
}
24 changes: 19 additions & 5 deletions src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@
{
RemoveColumnDefaultValue(table, definition.Name);
}
// Oracle rejects restating an existing NOT NULL constraint. Render type/default
// separately and change nullability only when its value actually changes.
// Oracle rejects LOB type declarations combined with nullability changes.
// Keep the type/default operation separate from the constraint operation.
definition.IsNullable = true;
var mapper = _dialect.GetAndMapColumnProperties(definition);
var sql = mapper.ColumnSql;
Expand All @@ -163,12 +163,26 @@
sql = sql[..^5];
}

if (existing.IsNullable != column.IsNullable)
var existingType = _dialect.GetColumnMapper(existing).Type;
if (mapper.Type is "CLOB" or "NCLOB" or "BLOB" && mapper.Type == existingType &&
definition.Collation == null && !definition.IsIdentity && !definition.IsUnsigned)
{
sql += column.IsNullable ? " NULL" : " NOT NULL";
// Restating an unchanged LOB type is unnecessary and can invoke Oracle's
// restricted LOB conversion path. Defaults still need to be applied.
if (definition.DefaultValue != null)
{
ChangeColumn(table, QuoteColumnNameIfRequired(definition.Name) + " " + _dialect.Default(definition.DefaultValue));
}
}
else
{
ChangeColumn(table, sql);
}

ChangeColumn(table, sql);
if (existing.IsNullable != column.IsNullable)
{
ChangeColumn(table, QuoteColumnNameIfRequired(column.Name) + (column.IsNullable ? " NULL" : " NOT NULL"));
}
}

private void CopyDataFromOneColumnToAnother(string table, string fromColumn, string toColumn)
Expand Down Expand Up @@ -281,7 +295,7 @@

public override string[] GetTables() => base.GetTables();

public override Column[] GetColumns(string table)

Check warning on line 298 in src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Unit)

'GetColumns' has a cyclomatic complexity of '52'. Rewrite or refactor the code to decrease its complexity below '26'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502)

Check warning on line 298 in src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (PostgreSQL)

'GetColumns' has a cyclomatic complexity of '52'. Rewrite or refactor the code to decrease its complexity below '26'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502)

Check warning on line 298 in src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs

View workflow job for this annotation

GitHub Actions / Test (Sybase)

'GetColumns' has a cyclomatic complexity of '52'. Rewrite or refactor the code to decrease its complexity below '26'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502)
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("SELECT");
Expand Down
Loading