diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs index 6ad3c5c1..f16ff560 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs +++ b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs @@ -1,468 +1,154 @@ using System; -using System.Data; +using System.Threading; +using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; -using Migrator.Tests.Providers.Base; +using DotNetProjects.Migrator.Providers; +using DotNetProjects.Migrator.Providers.Impl.Oracle; +using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; +using DotNetProjects.Migrator.Providers.Impl.SQLite; +using DotNetProjects.Migrator.Providers.Impl.SqlServer; +using DryIoc; +using Migrator.Tests.Database; +using Migrator.Tests.Database.Interfaces; +using Migrator.Tests.Settings; +using Migrator.Tests.Settings.Config; +using Migrator.Tests.Settings.Models; using NUnit.Framework; -namespace Migrator.Tests.Providers; +namespace Migrator.Tests.Providers.Base; /// -/// Base class for Provider tests for all non-constraint oriented tests. +/// Base class for provider tests. /// -public abstract class TransformationProviderBase : TransformationProviderSimpleBase +public abstract class TransformationProviderBase { - [Test] - public void TableExistsWorks() + [TearDown] + public virtual void TearDown() { - Assert.That(Provider.TableExists("gadadadadseeqwe"), Is.False); - Assert.That(Provider.TableExists("TestTwo"), Is.True); - } - - [Test] - public void ColumnExistsWorks() - { - Assert.That(Provider.ColumnExists("gadadadadseeqwe", "eqweqeq"), Is.False); - Assert.That(Provider.ColumnExists("TestTwo", "eqweqeq"), Is.False); - Assert.That(Provider.ColumnExists("TestTwo", "Id"), Is.True); - } + DropTestTables(); - [Test] - public void CanExecuteBadSqlForNonCurrentProvider() - { - Provider["foo"].ExecuteNonQuery("select foo from bar 123"); - } - - [Test] - public void TableCanBeAdded() - { - AddTable(); - Assert.That(Provider.TableExists("Test"), Is.True); + Provider?.Rollback(); } - [Test] - public void GetTablesWorks() + protected void DropTestTables() { - foreach (var name in Provider.GetTables()) + // Because MySql doesn't support schema transaction + // we got to remove the tables manually... sad... + try { - Provider.Logger.Log("Table: {0}", name); + Provider.RemoveTable("TestTwo"); } - - Assert.That(1, Is.EqualTo(Provider.GetTables().Length)); - AddTable(); - Assert.That(2, Is.EqualTo(Provider.GetTables().Length)); - } - - [Test] - public void GetColumnsReturnsProperCount() - { - AddTable(); - var cols = Provider.GetColumns("Test"); - - Assert.That(cols, Is.Not.Null); - Assert.That(6, Is.EqualTo(cols.Length)); - } - - [Test] - public void GetColumnsContainsProperNullInformation() - { - AddTableWithPrimaryKey(); - var cols = Provider.GetColumns("Test"); - Assert.That(cols, Is.Not.Null); - - foreach (var column in cols) + catch (Exception) { - if (column.Name == "name") - { - Assert.That((column.ColumnProperty & ColumnProperty.NotNull) == ColumnProperty.NotNull, Is.True); - } - else if (column.Name == "Title") - { - Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); - } } - } - - [Test] - public void CanAddTableWithPrimaryKey() - { - AddTableWithPrimaryKey(); - Assert.That(Provider.TableExists("Test"), Is.True); - } - - [Test] - public void RemoveTable() - { - AddTable(); - Provider.RemoveTable("Test"); - Assert.That(Provider.TableExists("Test"), Is.False); - } - - [Test] - public virtual void RenameTableThatExists() - { - AddTable(); - Provider.RenameTable("Test", "Test_Rename"); - - Assert.That(Provider.TableExists("Test_Rename"), Is.True); - Assert.That(Provider.TableExists("Test"), Is.False); - Provider.RemoveTable("Test_Rename"); - } - - [Test] - public void RenameTableToExistingTable() - { - AddTable(); - Assert.Throws(() => + try { - Provider.RenameTable("Test", "TestTwo"); - }); - } - - [Test] - public void RenameColumnThatExists() - { - AddTable(); - Provider.RenameColumn("Test", "name", "name_rename"); - - Assert.That(Provider.ColumnExists("Test", "name_rename"), Is.True); - Assert.That(Provider.ColumnExists("Test", "name"), Is.False); - } - - [Test] - public void RenameColumnToExistingColumn() - { - AddTable(); - Assert.Throws(() => + Provider.RemoveTable("Test"); + } + catch (Exception) { - Provider.RenameColumn("Test", "Title", "name"); - }); - } - - [Test] - public void RemoveUnexistingTable() - { - var exception = Assert.Catch(() => Provider.RemoveTable("abc")); - var expectedMessage = "Table with name 'abc' does not exist to rename"; - - Assert.That(exception.Message, Is.EqualTo(expectedMessage)); - } - - [Test] - public void AddColumn() - { - Provider.AddColumn("TestTwo", "Test", DbType.String, 50); - Assert.That(Provider.ColumnExists("TestTwo", "Test"), Is.True); - } - - [Test] - public void ChangeColumn() - { - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50)); - Assert.That(Provider.ColumnExists("TestTwo", "TestId"), Is.True); - Provider.Insert("TestTwo", ["Id", "TestId"], [1, "Not an Int val."]); - } - - [Test] - public void ChangeColumn_FromNullToNull() - { - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); - Provider.Insert("TestTwo", ["Id", "TestId"], [2, "Not an Int val."]); - } - - [Test] - public void AddDecimalColumn() - { - Provider.AddColumn("TestTwo", "TestDecimal", DbType.Decimal, 38); - Assert.That(Provider.ColumnExists("TestTwo", "TestDecimal"), Is.True); - } - - [Test] - public void AddColumnWithDefault() - { - Provider.AddColumn("TestTwo", "TestWithDefault", DbType.Int32, 50, 0, 10); - Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.True); - } - - [Test] - public void AddColumnWithDefaultButNoSize() - { - Provider.AddColumn("TestTwo", "TestWithDefault", DbType.Int32, 10); - Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.True); - - Provider.AddColumn("TestTwo", "TestWithDefaultString", DbType.String, "'foo'"); - Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefaultString"), Is.True); - } - - [Test] - public void AddBooleanColumnWithDefault() - { - Provider.AddColumn("TestTwo", "TestBoolean", DbType.Boolean, 0, 0, false); - Assert.That(Provider.ColumnExists("TestTwo", "TestBoolean"), Is.True); - } - - [Test] - public void CanGetNullableFromProvider() - { - Provider.AddColumn("TestTwo", "NullableColumn", DbType.String, 30, ColumnProperty.Null); - var columns = Provider.GetColumns("TestTwo"); - - foreach (var column in columns) + } + try + { + Provider.RemoveTable("SchemaInfo"); + } + catch (Exception) { - if (column.Name == "NullableColumn") - { - Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); - } } } - [Test] - public void RemoveColumn() - { - AddColumn(); - Provider.RemoveColumn("TestTwo", "Test"); - Assert.That(Provider.ColumnExists("TestTwo", "Test"), Is.False); - } - - [Test] - public void RemoveColumnWithDefault() - { - AddColumnWithDefault(); - Provider.RemoveColumn("TestTwo", "TestWithDefault"); - Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.False); - } - - [Test] - public void RemoveUnexistingColumn() - { - var exception1 = Assert.Throws(() => Provider.RemoveColumn("TestTwo", "abc")); - var exception2 = Assert.Throws(() => Provider.RemoveColumn("abc", "abc")); - - Assert.That(exception1.Message, Is.EqualTo("The table 'TestTwo' does not have a column named 'abc'")); - Assert.That(exception2.Message, Is.EqualTo("The table 'abc' does not exist")); - } - - /// - /// Supprimer une colonne bit causait une erreur à cause - /// de la valeur par défaut. - /// - [Test] - public void RemoveBoolColumn() - { - AddTable(); - Provider.AddColumn("Test", "Inactif", DbType.Boolean); - Assert.That(Provider.ColumnExists("Test", "Inactif"), Is.True); - - Provider.RemoveColumn("Test", "Inactif"); - Assert.That(Provider.ColumnExists("Test", "Inactif"), Is.False); - } - - [Test] - public void HasColumn() - { - AddColumn(); - Assert.That(Provider.ColumnExists("TestTwo", "Test"), Is.True); - Assert.That(Provider.ColumnExists("TestTwo", "TestPasLa"), Is.False); - } - - [Test] - public void HasTable() - { - Assert.That(Provider.TableExists("TestTwo"), Is.True); - } - - [Test] - public void AppliedMigrations() - { - Assert.That(Provider.TableExists("SchemaInfo"), Is.False); - - // Check that a "get" call works on the first run. - Assert.That(0, Is.EqualTo(Provider.AppliedMigrations.Count)); - Assert.That(Provider.TableExists("SchemaInfo"), Is.True, "No SchemaInfo table created"); - - // Check that a "set" called after the first run works. - Provider.MigrationApplied(1, null); - Assert.That(1, Is.EqualTo(Provider.AppliedMigrations[0])); - - Provider.RemoveTable("SchemaInfo"); - // Check that a "set" call works on the first run. - Provider.MigrationApplied(1, null); - Assert.That(1, Is.EqualTo(Provider.AppliedMigrations[0])); - Assert.That(Provider.TableExists("SchemaInfo"), Is.True, "No SchemaInfo table created"); - } - - - [Test] - public void CommitTwice() - { - Provider.Commit(); - Assert.That(0, Is.EqualTo(Provider.AppliedMigrations.Count)); - Provider.Commit(); - } - - [Test] - public void InsertData() - { - Provider.Insert("TestTwo", ["Id", "TestId"], [1, 1]); - Provider.Insert("TestTwo", ["Id", "TestId"], [2, 2]); - - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "TestId", "TestTwo"); - var vals = GetVals(reader); - - Assert.That(Array.Exists(vals, delegate (int val) { return val == 1; }), Is.True); - Assert.That(Array.Exists(vals, delegate (int val) { return val == 2; }), Is.True); - } + protected ITransformationProvider Provider; - [Test] - public void CanInsertNullData() + protected async Task BeginOracleTransactionAsync() { - AddTable(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var configReader = new ConfigurationReader(); - Provider.Insert("Test", ["Id", "Title"], [1, "foo"]); - Provider.Insert("Test", ["Id", "Title"], [2, null]); + var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.OracleId); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "Title", "Test"); - var vals = GetStringVals(reader); + var connectionString = databaseConnectionConfig?.ConnectionString; - Assert.That(Array.Exists(vals, delegate (string val) { return val == "foo"; }), Is.True); - Assert.That(Array.Exists(vals, delegate (string val) { return val == null; }), Is.True); - } + if (string.IsNullOrEmpty(connectionString)) + { + throw new IgnoreException($"No Oracle {nameof(DatabaseConnectionConfig.ConnectionString)} is set."); + } - [Test] - public void CanInsertDataWithSingleQuotes() - { - // Arrange - const string testString = "Test string with ' (single quote)"; - AddTable(); - Provider.Insert("Test", ["Id", "Title"], [1, testString]); + DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", () => Oracle.ManagedDataAccess.Client.OracleClientFactory.Instance); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "Title", "Test"); + using var container = new Container(); + container.RegisterDatabaseIntegrationTestService(); + var databaseIntegrationTestServiceFactory = container.Resolve(); + var oracleIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.Oracle); + var databaseInfo = await oracleIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, cts.Token); - Assert.That(reader.Read(), Is.True); - Assert.That(testString, Is.EqualTo(reader.GetString(0))); - Assert.That(reader.Read(), Is.False); - } + Provider = new OracleTransformationProvider(new OracleDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, null, "default", "Oracle.ManagedDataAccess.Client"); - [Test] - public void DeleteData() - { - InsertData(); - Provider.Delete("TestTwo", "TestId", "1"); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "TestId", "TestTwo"); - Assert.That(reader.Read(), Is.True); - Assert.That(2, Is.EqualTo(Convert.ToInt32(reader[0]))); - Assert.That(reader.Read(), Is.False); + Provider.BeginTransaction(); } - [Test] - public void DeleteDataWithArrays() + protected async Task BeginPostgreSQLTransactionAsync() { - InsertData(); - - Provider.Delete("TestTwo", ["TestId"], [1]); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var configReader = new ConfigurationReader(); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "TestId", "TestTwo"); + var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.PostgreSQL); - Assert.That(reader.Read(), Is.True); - Assert.That(2, Is.EqualTo(Convert.ToInt32(reader[0]))); - Assert.That(reader.Read(), Is.False); - } - - [Test] - public void UpdateData() - { - Provider.Insert("TestTwo", ["Id", "TestId"], [20, 1]); - Provider.Insert("TestTwo", ["Id", "TestId"], [21, 2]); + var connectionString = databaseConnectionConfig?.ConnectionString; - Provider.Update("TestTwo", ["TestId"], [3]); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "TestId", "TestTwo"); - var vals = GetVals(reader); + if (string.IsNullOrEmpty(connectionString)) + { + throw new IgnoreException("No Postgre SQL connection string is set."); + } - Assert.That(Array.Exists(vals, delegate (int val) { return val == 3; }), Is.True); - Assert.That(Array.Exists(vals, delegate (int val) { return val == 1; }), Is.False); - Assert.That(Array.Exists(vals, delegate (int val) { return val == 2; }), Is.False); - } + DbProviderFactories.RegisterFactory("Npgsql", () => Npgsql.NpgsqlFactory.Instance); - [Test] - public void CanUpdateWithNullData() - { - AddTable(); - Provider.Insert("Test", ["Id", "Title"], [1, "foo"]); - Provider.Insert("Test", ["Id", "Title"], [2, null]); + using var container = new Container(); + container.RegisterDatabaseIntegrationTestService(); + var databaseIntegrationTestServiceFactory = container.Resolve(); + var postgreIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.Postgres); + var databaseInfo = await postgreIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, cts.Token); - Provider.Update("Test", ["Title"], [null]); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "Title", "Test"); - var vals = GetStringVals(reader); + Provider = new PostgreSQLTransformationProvider(new PostgreSQLDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, null, "default", "Npgsql"); + Provider.BeginTransaction(); - Assert.That(vals[0], Is.Null); - Assert.That(vals[1], Is.Null); + await Task.CompletedTask; } - [Test] - public void UpdateDataWithWhere() + protected async Task BeginSQLiteTransactionAsync() { - Provider.Insert("TestTwo", ["Id", "TestId"], [10, 1]); - Provider.Insert("TestTwo", ["Id", "TestId"], [11, 2]); + var configReader = new ConfigurationReader(); + var connectionString = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.SQLiteId) + .ConnectionString; - Provider.Update("TestTwo", ["TestId"], [3], "TestId='1'"); - using var cmd = Provider.CreateCommand(); - using var reader = Provider.Select(cmd, "TestId", "TestTwo"); - var vals = GetVals(reader); + Provider = new SQLiteTransformationProvider(new SQLiteDialect(), connectionString, "default", null); + Provider.BeginTransaction(); - Assert.That(Array.Exists(vals, delegate (int val) { return val == 3; }), Is.True); - Assert.That(Array.Exists(vals, delegate (int val) { return val == 2; }), Is.True); - Assert.That(Array.Exists(vals, delegate (int val) { return val == 1; }), Is.False); + await Task.CompletedTask; } - [Test] - public void AddIndex() + protected async Task BeginSQLServerTransactionAsync() { - var indexName = "test_index"; - - Assert.That(Provider.IndexExists("TestTwo", indexName), Is.False); - Provider.AddIndex(indexName, "TestTwo", "Id", "TestId"); - Assert.That(Provider.IndexExists("TestTwo", indexName), Is.True); - } + var configReader = new ConfigurationReader(); - [Test] - public void RemoveIndex() - { - var indexName = "test_index"; + var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.SQLServerId); - Assert.That(Provider.IndexExists("TestTwo", indexName), Is.False); - Provider.AddIndex(indexName, "TestTwo", "Id", "TestId"); - Provider.RemoveIndex("TestTwo", indexName); - Assert.That(Provider.IndexExists("TestTwo", indexName), Is.False); - } + var connectionString = databaseConnectionConfig?.ConnectionString; + if (string.IsNullOrEmpty(connectionString)) + { + throw new IgnoreException($"No SQL Server {nameof(DatabaseConnectionConfig.ConnectionString)} is set."); + } - private int[] GetVals(IDataReader reader) - { - var vals = new int[2]; - Assert.That(reader.Read(), Is.True); - vals[0] = Convert.ToInt32(reader[0]); - Assert.That(reader.Read(), Is.True); - vals[1] = Convert.ToInt32(reader[0]); + DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", () => Microsoft.Data.SqlClient.SqlClientFactory.Instance); - return vals; - } + using var container = new Container(); + container.RegisterDatabaseIntegrationTestService(); + var databaseIntegrationTestServiceFactory = container.Resolve(); + var sqlServerIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.SQLServer); + var databaseInfo = await sqlServerIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, CancellationToken.None); - private string[] GetStringVals(IDataReader reader) - { - var vals = new string[2]; - Assert.That(reader.Read(), Is.True); - vals[0] = reader[0] as string; - Assert.That(reader.Read(), Is.True); - vals[1] = reader[0] as string; + Provider = new SqlServerTransformationProvider(new SqlServerDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, "dbo", "default", "Microsoft.Data.SqlClient"); - return vals; + Provider.BeginTransaction(); } } diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs b/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs index 7a599de4..40f4725f 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs +++ b/src/Migrator.Tests/Providers/Base/TransformationProviderSimpleBase.cs @@ -5,45 +5,8 @@ namespace Migrator.Tests.Providers.Base; -public abstract class TransformationProviderSimpleBase +public abstract class TransformationProviderSimpleBase : TransformationProviderBase { - protected ITransformationProvider Provider; - - [TearDown] - public virtual void TearDown() - { - DropTestTables(); - - Provider?.Rollback(); - } - - protected void DropTestTables() - { - // Because MySql doesn't support schema transaction - // we got to remove the tables manually... sad... - try - { - Provider.RemoveTable("TestTwo"); - } - catch (Exception) - { - } - try - { - Provider.RemoveTable("Test"); - } - catch (Exception) - { - } - try - { - Provider.RemoveTable("SchemaInfo"); - } - catch (Exception) - { - } - } - public void AddDefaultTable() { Provider.AddTable("TestTwo", @@ -75,4 +38,9 @@ public void AddTableWithPrimaryKey() new Column("bigstring", DbType.String, 50000) ); } + + public void AddPrimaryKey() + { + Provider.AddPrimaryKey("PK_Test", "Test", "Id"); + } } diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderConstraintBase.cs b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs similarity index 78% rename from src/Migrator.Tests/Providers/Base/TransformationProviderConstraintBase.cs rename to src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs index 8a096bcd..84ca3c1d 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderConstraintBase.cs +++ b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs @@ -2,14 +2,15 @@ using System.Data; using System.Linq; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.SQLite; using NUnit.Framework; -namespace Migrator.Tests.Providers; +namespace Migrator.Tests.Providers.Generic; /// -/// Base class for Provider tests for all tests including constraint oriented tests. +/// Base class for provider tests for all tests including constraint oriented tests. /// -public abstract class TransformationProviderConstraintBase : TransformationProviderBase +public abstract class TransformationProviderGenericMiscConstraintBase : TransformationProviderGenericMiscTests { public void AddForeignKey() { @@ -33,7 +34,7 @@ public void AddMultipleUniqueConstraint() Provider.AddUniqueConstraint("UN_Test_TestTwo", "TestTwo", "Id", "TestId"); } - public void AddCheckConstraint() + public void AddTestCheckConstraint() { Provider.AddCheckConstraint("CK_TestTwo_TestId", "TestTwo", "TestId>5"); } @@ -42,15 +43,23 @@ public void AddCheckConstraint() public void CanAddPrimaryKey() { AddPrimaryKey(); - Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True); - } - [Test] - public void AddIndexedColumn() - { - Provider.AddColumn("TestTwo", "Test", DbType.String, 50, ColumnProperty.Indexed); + if (Provider is SQLiteTransformationProvider) + { + Assert.Throws(() => Provider.PrimaryKeyExists("Test", "PK_Test")); + } + else + { + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True); + } } + // [Test] + // public void AddIndexedColumn() + // { + // Provider.AddColumn("TestTwo", "Test", DbType.String, 50, ColumnProperty.Indexed); + // } + [Test] public void AddUniqueColumn() { @@ -81,8 +90,10 @@ public virtual void CanAddMultipleUniqueConstraint() [Test] public virtual void CanAddCheckConstraint() { - AddCheckConstraint(); - Assert.That(Provider.ConstraintExists("TestTwo", "CK_TestTwo_TestId"), Is.True); + AddTestCheckConstraint(); + var constraintExists = Provider.ConstraintExists("TestTwo", "CK_TestTwo_TestId"); + + Assert.That(constraintExists, Is.True); } [Test] @@ -105,7 +116,7 @@ public void RemoveUniqueConstraint() [Test] public virtual void RemoveCheckConstraint() { - AddCheckConstraint(); + AddTestCheckConstraint(); Provider.RemoveConstraint("TestTwo", "CK_TestTwo_TestId"); Assert.That(Provider.ConstraintExists("TestTwo", "CK_TestTwo_TestId"), Is.False); } @@ -113,10 +124,22 @@ public virtual void RemoveCheckConstraint() [Test] public void RemoveUnexistingForeignKey() { + // Arrange AddForeignKey(); - Provider.RemoveForeignKey("abc", "FK_Test_TestTwo"); - Provider.RemoveForeignKey("abc", "abc"); - Provider.RemoveForeignKey("Test", "abc"); + + // Act/Assert + // Table does not exist. + Assert.Throws(() => Provider.RemoveForeignKey("NotExistingTable", "FK_Test_TestTwo")); + + // Table exists but foreign key does not exist. + if (Provider is SQLiteTransformationProvider) + { + Assert.Throws(() => Provider.RemoveForeignKey("Test", "NotExistingForeignKey")); + } + else + { + Assert.That(() => Provider.RemoveForeignKey("Test", "NotExistingForeignKey"), Throws.Exception); + } } [Test] @@ -124,32 +147,21 @@ public void ConstraintExist() { AddForeignKey(); Assert.That(Provider.ConstraintExists("TestTwo", "FK_Test_TestTwo"), Is.True); - Assert.That(Provider.ConstraintExists("abc", "abc"), Is.False); - } - - [Test] - public void AddTableWithCompoundPrimaryKey() - { - Provider.AddTable("Test", - new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) - ); - - Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); - Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); + Assert.That(Provider.ConstraintExists("TestTwo", "abc"), Is.False); } [Test] public void AddTableWithCompoundPrimaryKeyShouldKeepNullForOtherProperties() { - Provider.AddTable("Test", - new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey), - new Column("Name", DbType.String, 30, ColumnProperty.Null) - ); + var testTableName = "Test"; + + Provider.AddTable(testTableName, + new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("Name", DbType.String, 30, ColumnProperty.Null) + ); Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); - Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); var column = Provider.GetColumnByName("Test", "Name"); diff --git a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs new file mode 100644 index 00000000..65971c7e --- /dev/null +++ b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscTests.cs @@ -0,0 +1,474 @@ +using System; +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.Generic; + +/// +/// Base class for provider tests. +/// +public abstract class TransformationProviderGenericMiscTests : TransformationProviderSimpleBase +{ + [Test] + public void TableExistsWorks() + { + Assert.That(Provider.TableExists("gadadadadseeqwe"), Is.False); + Assert.That(Provider.TableExists("TestTwo"), Is.True); + } + + [Test] + public void ColumnExistsWorks() + { + Assert.That(Provider.ColumnExists("gadadadadseeqwe", "eqweqeq"), Is.False); + Assert.That(Provider.ColumnExists("TestTwo", "eqweqeq"), Is.False); + Assert.That(Provider.ColumnExists("TestTwo", "Id"), Is.True); + } + + [Test] + public void CanExecuteBadSqlForNonCurrentProvider() + { + Provider["foo"].ExecuteNonQuery("select foo from bar 123"); + } + + [Test] + public void TableCanBeAdded() + { + AddTable(); + Assert.That(Provider.TableExists("Test"), Is.True); + } + + [Test] + public void GetTablesWorks() + { + var tables = Provider.GetTables(); + + foreach (var name in tables) + { + Provider.Logger.Log("Table: {0}", name); + } + + Assert.That(1, Is.EqualTo(tables.Length)); + AddTable(); + + tables = Provider.GetTables(); + + Assert.That(2, Is.EqualTo(tables.Length)); + } + + [Test] + public void GetColumnsReturnsProperCount() + { + AddTable(); + var cols = Provider.GetColumns("Test"); + + Assert.That(cols, Is.Not.Null); + Assert.That(6, Is.EqualTo(cols.Length)); + } + + [Test] + public void GetColumnsContainsProperNullInformation() + { + AddTableWithPrimaryKey(); + var cols = Provider.GetColumns("Test"); + Assert.That(cols, Is.Not.Null); + + foreach (var column in cols) + { + if (column.Name == "name") + { + Assert.That((column.ColumnProperty & ColumnProperty.NotNull) == ColumnProperty.NotNull, Is.True); + } + else if (column.Name == "Title") + { + Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); + } + } + } + + [Test] + public void CanAddTableWithPrimaryKey() + { + AddTableWithPrimaryKey(); + Assert.That(Provider.TableExists("Test"), Is.True); + } + + [Test] + public void RemoveTable() + { + AddTable(); + Provider.RemoveTable("Test"); + Assert.That(Provider.TableExists("Test"), Is.False); + } + + [Test] + public virtual void RenameTableThatExists() + { + AddTable(); + Provider.RenameTable("Test", "Test_Rename"); + + Assert.That(Provider.TableExists("Test_Rename"), Is.True); + Assert.That(Provider.TableExists("Test"), Is.False); + Provider.RemoveTable("Test_Rename"); + } + + [Test] + public void RenameTableToExistingTable() + { + AddTable(); + Assert.Throws(() => + { + Provider.RenameTable("Test", "TestTwo"); + }); + } + + [Test] + public void RenameColumnThatExists() + { + AddTable(); + Provider.RenameColumn("Test", "name", "name_rename"); + + Assert.That(Provider.ColumnExists("Test", "name_rename"), Is.True); + Assert.That(Provider.ColumnExists("Test", "name"), Is.False); + } + + [Test] + public void RenameColumnToExistingColumn() + { + AddTable(); + Assert.Throws(() => + { + Provider.RenameColumn("Test", "Title", "name"); + }); + } + + [Test] + public void RemoveUnexistingTable() + { + var exception = Assert.Catch(() => Provider.RemoveTable("abc")); + var expectedMessage = "Table with name 'abc' does not exist to rename"; + + Assert.That(exception.Message, Is.EqualTo(expectedMessage)); + } + + [Test] + public void AddColumn() + { + Provider.AddColumn("TestTwo", "Test", DbType.String, 50); + Assert.That(Provider.ColumnExists("TestTwo", "Test"), Is.True); + } + + [Test] + public void ChangeColumn() + { + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50)); + Assert.That(Provider.ColumnExists("TestTwo", "TestId"), Is.True); + Provider.Insert("TestTwo", ["Id", "TestId"], [1, "Not an Int val."]); + } + + [Test] + public void ChangeColumn_FromNullToNull() + { + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); + Provider.Insert("TestTwo", ["Id", "TestId"], [2, "Not an Int val."]); + } + + [Test] + public void AddDecimalColumn() + { + Provider.AddColumn("TestTwo", "TestDecimal", DbType.Decimal, 38); + Assert.That(Provider.ColumnExists("TestTwo", "TestDecimal"), Is.True); + } + + [Test] + public void AddColumnWithDefault() + { + Provider.AddColumn("TestTwo", "TestWithDefault", DbType.Int32, 50, 0, 10); + Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.True); + } + + [Test] + public void AddColumnWithDefaultButNoSize() + { + Provider.AddColumn("TestTwo", "TestWithDefault", DbType.Int32, 10); + Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.True); + + Provider.AddColumn("TestTwo", "TestWithDefaultString", DbType.String, "'foo'"); + Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefaultString"), Is.True); + } + + [Test] + public void AddBooleanColumnWithDefault() + { + Provider.AddColumn("TestTwo", "TestBoolean", DbType.Boolean, 0, 0, false); + Assert.That(Provider.ColumnExists("TestTwo", "TestBoolean"), Is.True); + } + + [Test] + public void CanGetNullableFromProvider() + { + Provider.AddColumn("TestTwo", "NullableColumn", DbType.String, 30, ColumnProperty.Null); + var columns = Provider.GetColumns("TestTwo"); + + foreach (var column in columns) + { + if (column.Name == "NullableColumn") + { + Assert.That((column.ColumnProperty & ColumnProperty.Null) == ColumnProperty.Null, Is.True); + } + } + } + + [Test] + public void RemoveColumn() + { + AddColumn(); + Provider.RemoveColumn("TestTwo", "Test"); + Assert.That(Provider.ColumnExists("TestTwo", "Test"), Is.False); + } + + [Test] + public void RemoveColumnWithDefault() + { + AddColumnWithDefault(); + Provider.RemoveColumn("TestTwo", "TestWithDefault"); + Assert.That(Provider.ColumnExists("TestTwo", "TestWithDefault"), Is.False); + } + + [Test] + public void RemoveUnexistingColumn() + { + var exception1 = Assert.Throws(() => Provider.RemoveColumn("TestTwo", "abc")); + var exception2 = Assert.Throws(() => Provider.RemoveColumn("abc", "abc")); + + Assert.That(exception1.Message, Is.EqualTo("The table 'TestTwo' does not have a column named 'abc'")); + Assert.That(exception2.Message, Is.EqualTo("The table 'abc' does not exist")); + } + + /// + /// Supprimer une colonne bit causait une erreur à cause + /// de la valeur par défaut. + /// + [Test] + public void RemoveBoolColumn() + { + AddTable(); + Provider.AddColumn("Test", "Inactif", DbType.Boolean); + Assert.That(Provider.ColumnExists("Test", "Inactif"), Is.True); + + Provider.RemoveColumn("Test", "Inactif"); + Assert.That(Provider.ColumnExists("Test", "Inactif"), Is.False); + } + + [Test] + public void HasColumn() + { + AddColumn(); + Assert.That(Provider.ColumnExists("TestTwo", "Test"), Is.True); + Assert.That(Provider.ColumnExists("TestTwo", "TestPasLa"), Is.False); + } + + [Test] + public void HasTable() + { + Assert.That(Provider.TableExists("TestTwo"), Is.True); + } + + [Test] + public void AppliedMigrations() + { + Assert.That(Provider.TableExists("SchemaInfo"), Is.False); + + // Check that a "get" call works on the first run. + Assert.That(0, Is.EqualTo(Provider.AppliedMigrations.Count)); + Assert.That(Provider.TableExists("SchemaInfo"), Is.True, "No SchemaInfo table created"); + + // Check that a "set" called after the first run works. + Provider.MigrationApplied(1, null); + Assert.That(1, Is.EqualTo(Provider.AppliedMigrations[0])); + + Provider.RemoveTable("SchemaInfo"); + // Check that a "set" call works on the first run. + Provider.MigrationApplied(1, null); + Assert.That(1, Is.EqualTo(Provider.AppliedMigrations[0])); + Assert.That(Provider.TableExists("SchemaInfo"), Is.True, "No SchemaInfo table created"); + } + + + [Test] + public void CommitTwice() + { + Provider.Commit(); + Assert.That(0, Is.EqualTo(Provider.AppliedMigrations.Count)); + Provider.Commit(); + } + + [Test] + public void InsertData() + { + Provider.Insert("TestTwo", ["Id", "TestId"], [1, 1]); + Provider.Insert("TestTwo", ["Id", "TestId"], [2, 2]); + + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "TestId", "TestTwo"); + var vals = GetVals(reader); + + Assert.That(Array.Exists(vals, delegate (int val) { return val == 1; }), Is.True); + Assert.That(Array.Exists(vals, delegate (int val) { return val == 2; }), Is.True); + } + + [Test] + public void CanInsertNullData() + { + AddTable(); + + Provider.Insert("Test", ["Id", "Title"], [1, "foo"]); + Provider.Insert("Test", ["Id", "Title"], [2, null]); + + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "Title", "Test"); + var vals = GetStringVals(reader); + + Assert.That(Array.Exists(vals, delegate (string val) { return val == "foo"; }), Is.True); + Assert.That(Array.Exists(vals, delegate (string val) { return val == null; }), Is.True); + } + + [Test] + public void CanInsertDataWithSingleQuotes() + { + // Arrange + const string testString = "Test string with ' (single quote)"; + AddTable(); + Provider.Insert("Test", ["Id", "Title"], [1, testString]); + + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "Title", "Test"); + + Assert.That(reader.Read(), Is.True); + Assert.That(testString, Is.EqualTo(reader.GetString(0))); + Assert.That(reader.Read(), Is.False); + } + + [Test] + public void DeleteData() + { + InsertData(); + Provider.Delete("TestTwo", "TestId", "1"); + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "TestId", "TestTwo"); + Assert.That(reader.Read(), Is.True); + Assert.That(2, Is.EqualTo(Convert.ToInt32(reader[0]))); + Assert.That(reader.Read(), Is.False); + } + + [Test] + public void DeleteDataWithArrays() + { + InsertData(); + + Provider.Delete("TestTwo", ["TestId"], [1]); + + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "TestId", "TestTwo"); + + Assert.That(reader.Read(), Is.True); + Assert.That(2, Is.EqualTo(Convert.ToInt32(reader[0]))); + Assert.That(reader.Read(), Is.False); + } + + [Test] + public void UpdateData() + { + Provider.Insert("TestTwo", ["Id", "TestId"], [20, 1]); + Provider.Insert("TestTwo", ["Id", "TestId"], [21, 2]); + + Provider.Update("TestTwo", ["TestId"], [3]); + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "TestId", "TestTwo"); + var vals = GetVals(reader); + + Assert.That(Array.Exists(vals, delegate (int val) { return val == 3; }), Is.True); + Assert.That(Array.Exists(vals, delegate (int val) { return val == 1; }), Is.False); + Assert.That(Array.Exists(vals, delegate (int val) { return val == 2; }), Is.False); + } + + [Test] + public void CanUpdateWithNullData() + { + AddTable(); + Provider.Insert("Test", ["Id", "Title"], [1, "foo"]); + Provider.Insert("Test", ["Id", "Title"], [2, null]); + + Provider.Update("Test", ["Title"], [null]); + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "Title", "Test"); + var vals = GetStringVals(reader); + + Assert.That(vals[0], Is.Null); + Assert.That(vals[1], Is.Null); + } + + [Test] + public void UpdateDataWithWhere() + { + Provider.Insert("TestTwo", ["Id", "TestId"], [10, 1]); + Provider.Insert("TestTwo", ["Id", "TestId"], [11, 2]); + + Provider.Update("TestTwo", ["TestId"], [3], "TestId='1'"); + using var cmd = Provider.CreateCommand(); + using var reader = Provider.Select(cmd, "TestId", "TestTwo"); + var vals = GetVals(reader); + + Assert.That(Array.Exists(vals, delegate (int val) { return val == 3; }), Is.True); + Assert.That(Array.Exists(vals, delegate (int val) { return val == 2; }), Is.True); + Assert.That(Array.Exists(vals, delegate (int val) { return val == 1; }), Is.False); + } + + [Test] + public void AddIndex() + { + var indexName = "test_index"; + + Assert.That(Provider.IndexExists("TestTwo", indexName), Is.False); + Provider.AddIndex(indexName, "TestTwo", "Id", "TestId"); + Assert.That(Provider.IndexExists("TestTwo", indexName), Is.True); + } + + [Test] + public void RemoveIndex() + { + var indexName = "test_index"; + + Assert.That(Provider.IndexExists("TestTwo", indexName), Is.False); + Provider.AddIndex(indexName, "TestTwo", "Id", "TestId"); + Provider.RemoveIndex("TestTwo", indexName); + Assert.That(Provider.IndexExists("TestTwo", indexName), Is.False); + } + + + private int[] GetVals(IDataReader reader) + { + var vals = new int[2]; + Assert.That(reader.Read(), Is.True); + vals[0] = Convert.ToInt32(reader[0]); + Assert.That(reader.Read(), Is.True); + vals[1] = Convert.ToInt32(reader[0]); + + return vals; + } + + private string[] GetStringVals(IDataReader reader) + { + var vals = new string[2]; + Assert.That(reader.Read(), Is.True); + vals[0] = reader[0] as string; + Assert.That(reader.Read(), Is.True); + vals[1] = reader[0] as string; + + return vals; + } +} diff --git a/src/Migrator.Tests/Providers/OracleProvider/Base/OracleTransformationProviderTestBase.cs b/src/Migrator.Tests/Providers/OracleProvider/Base/OracleTransformationProviderTestBase.cs index a7179ec1..8839dc37 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/Base/OracleTransformationProviderTestBase.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/Base/OracleTransformationProviderTestBase.cs @@ -19,28 +19,7 @@ public class OracleTransformationProviderTestBase : TransformationProviderSimple [SetUp] public async Task SetUpAsync() { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var configReader = new ConfigurationReader(); - - var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.OracleId); - - var connectionString = databaseConnectionConfig?.ConnectionString; - - if (string.IsNullOrEmpty(connectionString)) - { - throw new IgnoreException($"No Oracle {nameof(DatabaseConnectionConfig.ConnectionString)} is set."); - } - - DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", () => Oracle.ManagedDataAccess.Client.OracleClientFactory.Instance); - - using var container = new Container(); - container.RegisterDatabaseIntegrationTestService(); - var databaseIntegrationTestServiceFactory = container.Resolve(); - var oracleIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.Oracle); - var databaseInfo = await oracleIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, cts.Token); - - Provider = new OracleTransformationProvider(new OracleDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, null, "default", "Oracle.ManagedDataAccess.Client"); - Provider.BeginTransaction(); + await BeginOracleTransactionAsync(); AddDefaultTable(); } diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs new file mode 100644 index 00000000..d7ffcdce --- /dev/null +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderGenericTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers; +using DotNetProjects.Migrator.Providers.Impl.Oracle; +using DryIoc; +using Migrator.Tests.Database; +using Migrator.Tests.Database.Interfaces; +using Migrator.Tests.Providers.Generic; +using Migrator.Tests.Settings; +using Migrator.Tests.Settings.Config; +using Migrator.Tests.Settings.Models; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.OracleProvider; + +[TestFixture] +[Category("Oracle")] +public class OracleTransformationProviderGenericTests : TransformationProviderGenericMiscConstraintBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginOracleTransactionAsync(); + + AddDefaultTable(); + } + + [Test] + public void ChangeColumn_FromNotNullToNotNull() + { + Provider.ExecuteNonQuery("DELETE FROM TestTwo"); + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); + Provider.Insert("TestTwo", ["Id", "TestId"], [3, "Not an Int val."]); + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.NotNull)); + Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.NotNull)); + } +} diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderTest.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderTest.cs deleted file mode 100644 index 1a582756..00000000 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProviderTest.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Data; -using System.Threading; -using System.Threading.Tasks; -using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Providers; -using DotNetProjects.Migrator.Providers.Impl.Oracle; -using DryIoc; -using Migrator.Tests.Database; -using Migrator.Tests.Database.Interfaces; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; -using Migrator.Tests.Settings.Models; -using NUnit.Framework; - -namespace Migrator.Tests.Providers.OracleProvider; - -[TestFixture] -[Category("Oracle")] -public class OracleTransformationProviderTest : TransformationProviderConstraintBase -{ - [SetUp] - public async Task SetUpAsync() - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var configReader = new ConfigurationReader(); - - var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.OracleId); - - var connectionString = databaseConnectionConfig?.ConnectionString; - - if (string.IsNullOrEmpty(connectionString)) - { - throw new IgnoreException($"No Oracle {nameof(DatabaseConnectionConfig.ConnectionString)} is set."); - } - - DbProviderFactories.RegisterFactory("Oracle.ManagedDataAccess.Client", () => Oracle.ManagedDataAccess.Client.OracleClientFactory.Instance); - - using var container = new Container(); - container.RegisterDatabaseIntegrationTestService(); - var databaseIntegrationTestServiceFactory = container.Resolve(); - var oracleIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.Oracle); - var databaseInfo = await oracleIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, cts.Token); - - Provider = new OracleTransformationProvider(new OracleDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, null, "default", "Oracle.ManagedDataAccess.Client"); - Provider.BeginTransaction(); - - AddDefaultTable(); - } - - [Test] - public void ChangeColumn_FromNotNullToNotNull() - { - Provider.ExecuteNonQuery("DELETE FROM TestTwo"); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.Null)); - Provider.Insert("TestTwo", ["Id", "TestId"], [3, "Not an Int val."]); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.NotNull)); - Provider.ChangeColumn("TestTwo", new Column("TestId", DbType.String, 50, ColumnProperty.NotNull)); - } -} diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddTable_Tests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddTable_Tests.cs new file mode 100644 index 00000000..aa33b236 --- /dev/null +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddTable_Tests.cs @@ -0,0 +1,30 @@ +using System.Data; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.OracleProvider; + +[TestFixture] +[Category("Oracle")] +public class OracleTransformationProvider_AddTable_Tests : TransformationProviderBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginOracleTransactionAsync(); + } + + [Test] + public void AddTableWithCompoundPrimaryKey() + { + Provider.AddTable("Test", + new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) + ); + + Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs new file mode 100644 index 00000000..7993fbba --- /dev/null +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_GetColumns_Tests.cs @@ -0,0 +1,114 @@ +using System; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Base; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.OracleProvider; + +[TestFixture] +[Category("Oracle")] +public class OracleTransformationProvider_GetColumns_Tests : TransformationProviderBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginOracleTransactionAsync(); + } + + /// + /// Since SQLite does not support binary default values in the generic file a separate test is needed for Oracle + /// Find the generic test in the base class. + /// + [Test] + public void GetColumns_Oracle_DefaultValues_Succeeds() + { + // Arrange + const string testTableName = "MyDefaultTestTable"; + const string binaryColumnName1 = "binarycolumn1"; + + // Should be extended by remaining types + Provider.AddTable(testTableName, + new Column(binaryColumnName1, DbType.Binary, defaultValue: new byte[] { 12, 32, 34 }) + ); + + // Act + var columns = Provider.GetColumns(testTableName); + + // Assert + var binarycolumn1 = columns.Single(x => x.Name.Equals(binaryColumnName1, StringComparison.OrdinalIgnoreCase)); + + Assert.That(binarycolumn1.DefaultValue, Is.EqualTo(new byte[] { 12, 32, 34 })); + } + + [Test] + public void GetColumns_DefaultValues_Succeeds() + { + // Arrange + var dateTimeDefaultValue = new DateTime(2000, 1, 2, 3, 4, 5, DateTimeKind.Utc); + var guidDefaultValue = Guid.NewGuid(); + var decimalDefaultValue = 14.56565m; + + const string testTableName = "MyDefaultTestTable"; + + const string dateTimeColumnName1 = "datetimecolumn1"; + const string dateTimeColumnName2 = "datetimecolumn2"; + const string decimalColumnName1 = "decimalcolumn"; + const string guidColumnName1 = "guidcolumn1"; + const string booleanColumnName1 = "booleancolumn1"; + const string int32ColumnName1 = "int32column1"; + const string int64ColumnName1 = "int64column1"; + const string int64ColumnName2 = "int64column2"; + const string stringColumnName1 = "stringcolumn1"; + const string binaryColumnName1 = "binarycolumn1"; + const string doubleColumnName1 = "doublecolumn1"; + + // Should be extended by remaining types + Provider.AddTable(testTableName, + new Column(dateTimeColumnName1, DbType.DateTime, dateTimeDefaultValue), + new Column(dateTimeColumnName2, DbType.DateTime2, dateTimeDefaultValue), + new Column(decimalColumnName1, DbType.Decimal, decimalDefaultValue), + new Column(guidColumnName1, DbType.Guid, guidDefaultValue), + + // other boolean default values are tested in another test + new Column(booleanColumnName1, DbType.Boolean, true), + + new Column(int32ColumnName1, DbType.Int32, defaultValue: 43), + new Column(int64ColumnName1, DbType.Int64, defaultValue: 88), + new Column(int64ColumnName2, DbType.Int64, defaultValue: 0), + new Column(stringColumnName1, DbType.String, defaultValue: "Hello"), + new Column(binaryColumnName1, DbType.Binary, defaultValue: new byte[] { 12, 32, 34 }), + new Column(doubleColumnName1, DbType.Double, defaultValue: 84.874596567) { Precision = 19, Scale = 10 } + ); + + // Act + var columns = Provider.GetColumns(testTableName); + + // Assert + var dateTimeColumn1 = columns.Single(x => x.Name.Equals(dateTimeColumnName1, StringComparison.OrdinalIgnoreCase)); + var dateTimeColumn2 = columns.Single(x => x.Name.Equals(dateTimeColumnName2, StringComparison.OrdinalIgnoreCase)); + var decimalColumn1 = columns.Single(x => x.Name.Equals(decimalColumnName1, StringComparison.OrdinalIgnoreCase)); + var guidColumn1 = columns.Single(x => x.Name.Equals(guidColumnName1, StringComparison.OrdinalIgnoreCase)); + var booleanColumn1 = columns.Single(x => x.Name.Equals(booleanColumnName1, StringComparison.OrdinalIgnoreCase)); + var int32Column1 = columns.Single(x => x.Name.Equals(int32ColumnName1, StringComparison.OrdinalIgnoreCase)); + var int64Column1 = columns.Single(x => x.Name.Equals(int64ColumnName1, StringComparison.OrdinalIgnoreCase)); + var int64Column2 = columns.Single(x => x.Name.Equals(int64ColumnName2, StringComparison.OrdinalIgnoreCase)); + var stringColumn1 = columns.Single(x => x.Name.Equals(stringColumnName1, StringComparison.OrdinalIgnoreCase)); + var binarycolumn1 = columns.Single(x => x.Name.Equals(binaryColumnName1, StringComparison.OrdinalIgnoreCase)); + var doubleColumn1 = columns.Single(x => x.Name.Equals(doubleColumnName1, StringComparison.OrdinalIgnoreCase)); + + Assert.That(dateTimeColumn1.DefaultValue, Is.EqualTo(dateTimeDefaultValue)); + Assert.That(dateTimeColumn2.DefaultValue, Is.EqualTo(dateTimeDefaultValue)); + Assert.That(decimalColumn1.DefaultValue, Is.EqualTo(decimalDefaultValue)); + Assert.That(guidColumn1.DefaultValue, Is.EqualTo(guidDefaultValue)); + Assert.That(booleanColumn1.DefaultValue, Is.True); + Assert.That(int32Column1.DefaultValue, Is.EqualTo(43)); + Assert.That(int64Column1.DefaultValue, Is.EqualTo(88)); + Assert.That(stringColumn1.DefaultValue, Is.EqualTo("Hello")); + Assert.That(binarycolumn1.DefaultValue, Is.EqualTo(new byte[] { 12, 32, 34 })); + Assert.That(doubleColumn1.DefaultValue, Is.EqualTo(84.874596567)); + } +} diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_PrimaryKeyExists.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_PrimaryKeyExists.cs new file mode 100644 index 00000000..54d35731 --- /dev/null +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_PrimaryKeyExists.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.OracleProvider; + +[TestFixture] +[Category("Oracle")] +public class OracleTransformationProvider_PrimaryKeyExistsTests : TransformationProviderSimpleBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginOracleTransactionAsync(); + } + + [Test] + public void CanAddPrimaryKey() + { + AddTable(); + AddPrimaryKey(); + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/Base/PostgreSQLTransformationProviderTestBase.cs b/src/Migrator.Tests/Providers/PostgreSQL/Base/PostgreSQLTransformationProviderTestBase.cs index d8ce1eb7..9e8de2ac 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/Base/PostgreSQLTransformationProviderTestBase.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/Base/PostgreSQLTransformationProviderTestBase.cs @@ -1,33 +1,16 @@ -using DotNetProjects.Migrator.Providers; -using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; +using System.Threading.Tasks; using Migrator.Tests.Providers.Base; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; using NUnit.Framework; -namespace Migrator.Tests.Providers.PostgreSQL; +namespace Migrator.Tests.Providers.PostgreSQL.Base; [TestFixture] [Category("Postgre")] public abstract class PostgreSQLTransformationProviderTestBase : TransformationProviderSimpleBase { [SetUp] - public void SetUp() + public async Task SetUpAsync() { - var configReader = new ConfigurationReader(); - var connectionString = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.PostgreSQL) - ?.ConnectionString; - - if (string.IsNullOrEmpty(connectionString)) - { - throw new IgnoreException("No Postgre ConnectionString is Set."); - } - - DbProviderFactories.RegisterFactory("Npgsql", () => Npgsql.NpgsqlFactory.Instance); - - Provider = new PostgreSQLTransformationProvider(new PostgreSQLDialect(), connectionString, null, "default", "Npgsql"); - Provider.BeginTransaction(); - - AddDefaultTable(); + await BeginPostgreSQLTransactionAsync(); } } diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProviderGenericTests.cs new file mode 100644 index 00000000..5fce7dfd --- /dev/null +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProviderGenericTests.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.PostgreSQL; + +[TestFixture] +[Category("Postgre")] +public class PostgreSQLTransformationProviderGenericTests : TransformationProviderGenericMiscConstraintBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginPostgreSQLTransactionAsync(); + + AddDefaultTable(); + } +} diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProviderTest.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProviderTest.cs deleted file mode 100644 index a1e973ec..00000000 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProviderTest.cs +++ /dev/null @@ -1,32 +0,0 @@ -using DotNetProjects.Migrator.Providers; -using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; -using NUnit.Framework; - -namespace Migrator.Tests.Providers; - -[TestFixture] -[Category("Postgre")] -public class PostgreSQLTransformationProviderTest : TransformationProviderConstraintBase -{ - [SetUp] - public void SetUp() - { - var configReader = new ConfigurationReader(); - var connectionString = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.PostgreSQL) - ?.ConnectionString; - - if (string.IsNullOrEmpty(connectionString)) - { - throw new IgnoreException("No Postgre ConnectionString is Set."); - } - - DbProviderFactories.RegisterFactory("Npgsql", () => Npgsql.NpgsqlFactory.Instance); - - Provider = new PostgreSQLTransformationProvider(new PostgreSQLDialect(), connectionString, null, "default", "Npgsql"); - Provider.BeginTransaction(); - - AddDefaultTable(); - } -} diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddTableTests.cs new file mode 100644 index 00000000..7095591f --- /dev/null +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddTableTests.cs @@ -0,0 +1,24 @@ +using System; +using System.Data; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.PostgreSQL; + +[TestFixture] +[Category("Postgre")] +public class PostgreSQLTransformationProvider_AddTableTests : PostgreSQLTransformationProviderTestBase +{ + [Test] + public void AddTableWithCompoundPrimaryKey() + { + Provider.AddTable("Test", + new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) + ); + + Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); + } +} diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnContentSizeTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnContent_SizeTests.cs similarity index 94% rename from src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnContentSizeTests.cs rename to src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnContent_SizeTests.cs index d2a3701f..16c9deda 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnContentSizeTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnContent_SizeTests.cs @@ -1,13 +1,14 @@ using System; using System.Data; using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; using NUnit.Framework; namespace Migrator.Tests.Providers.PostgreSQL; [TestFixture] [Category("Postgre")] -public class PostgreSQLTransformationProvider_GetColumnContentSizeTests : PostgreSQLTransformationProviderTestBase +public class PostgreSQLTransformationProvider_GetColumnContentSize_Tests : PostgreSQLTransformationProviderTestBase { [Test] public void GetColumnContentSize_UseStringColumn_MaxContentLengthIsCorrect() diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsTypeTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsTypeTests.cs index de7cf454..dd9e1e53 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsTypeTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsTypeTests.cs @@ -1,6 +1,7 @@ using System.Data; using System.Linq; using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; using NUnit.Framework; namespace Migrator.Tests.Providers.PostgreSQL; diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsDefaultValueTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs similarity index 63% rename from src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsDefaultValueTests.cs rename to src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs index 0e9447b6..1fe66431 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumnsDefaultValueTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_DefaultValueTests.cs @@ -1,16 +1,55 @@ using System; using System.Data; using System.Linq; +using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Base; +using Migrator.Tests.Providers.Generic; using NUnit.Framework; namespace Migrator.Tests.Providers.PostgreSQL; [TestFixture] [Category("Postgre")] -public class PostgreSQLTransformationProvider_GetColumnsDefaultTypeTests : PostgreSQLTransformationProviderTestBase +public class PostgreSQLTransformationProvider_GetColumns_DefaultValuesTests : TransformationProviderBase { - private const decimal DecimalDefaultValue = 14.56565m; + [SetUp] + public async Task SetUpAsync() + { + await BeginPostgreSQLTransactionAsync(); + } + + /// + /// More tests for GetColumns in + /// + [Test] + public void GetColumns_Postgres_DefaultValues_Succeeds() + { + // Arrange + const string testTableName = "MyDefaultTestTable"; + const string intervalColumnName1 = "intervalcolumn1"; + const string intervalColumnName2 = "intervalcolumn2"; + const string binaryColumnName1 = "binarycolumn1"; + + // Should be extended by remaining types + Provider.AddTable(testTableName, + new Column(intervalColumnName1, MigratorDbType.Interval, defaultValue: new TimeSpan(100000, 3, 4, 5, 666)), + new Column(intervalColumnName2, MigratorDbType.Interval, defaultValue: new TimeSpan(0, 0, 0, 0, 666)), + new Column(binaryColumnName1, DbType.Binary, defaultValue: new byte[] { 12, 32, 34 }) + ); + + // Act + var columns = Provider.GetColumns(testTableName); + + // Assert + var intervalColumn1 = columns.Single(x => x.Name == intervalColumnName1); + var intervalColumn2 = columns.Single(x => x.Name == intervalColumnName2); + var binarycolumn1 = columns.Single(x => x.Name.Equals(binaryColumnName1, StringComparison.OrdinalIgnoreCase)); + + Assert.That(intervalColumn1.DefaultValue, Is.EqualTo(new TimeSpan(100000, 3, 4, 5, 666))); + Assert.That(intervalColumn2.DefaultValue, Is.EqualTo(new TimeSpan(0, 0, 0, 0, 666))); + Assert.That(binarycolumn1.DefaultValue, Is.EqualTo(new byte[] { 12, 32, 34 })); + } [Test] public void GetColumns_DefaultValues_Succeeds() @@ -18,6 +57,7 @@ public void GetColumns_DefaultValues_Succeeds() // Arrange var dateTimeDefaultValue = new DateTime(2000, 1, 2, 3, 4, 5, DateTimeKind.Utc); var guidDefaultValue = Guid.NewGuid(); + var decimalDefaultValue = 14.56565m; const string testTableName = "MyDefaultTestTable"; @@ -32,14 +72,12 @@ public void GetColumns_DefaultValues_Succeeds() const string stringColumnName1 = "stringcolumn1"; const string binaryColumnName1 = "binarycolumn1"; const string doubleColumnName1 = "doublecolumn1"; - const string intervalColumnName1 = "intervalcolumn1"; - const string intervalColumnName2 = "intervalcolumn2"; // Should be extended by remaining types Provider.AddTable(testTableName, new Column(dateTimeColumnName1, DbType.DateTime, dateTimeDefaultValue), new Column(dateTimeColumnName2, DbType.DateTime2, dateTimeDefaultValue), - new Column(decimalColumnName1, DbType.Decimal, DecimalDefaultValue), + new Column(decimalColumnName1, DbType.Decimal, decimalDefaultValue), new Column(guidColumnName1, DbType.Guid, guidDefaultValue), // other boolean default values are tested in another test @@ -50,41 +88,35 @@ public void GetColumns_DefaultValues_Succeeds() new Column(int64ColumnName2, DbType.Int64, defaultValue: 0), new Column(stringColumnName1, DbType.String, defaultValue: "Hello"), new Column(binaryColumnName1, DbType.Binary, defaultValue: new byte[] { 12, 32, 34 }), - new Column(doubleColumnName1, DbType.Double, defaultValue: 84.874596565), - new Column(intervalColumnName1, MigratorDbType.Interval, defaultValue: new TimeSpan(100000, 3, 4, 5, 666)), - new Column(intervalColumnName2, MigratorDbType.Interval, defaultValue: new TimeSpan(0, 0, 0, 0, 666)) + new Column(doubleColumnName1, DbType.Double, defaultValue: 84.874596567) { Precision = 19, Scale = 10 } ); // Act var columns = Provider.GetColumns(testTableName); // Assert - var dateTimeColumn1 = columns.Single(x => x.Name == dateTimeColumnName1); - var dateTimeColumn2 = columns.Single(x => x.Name == dateTimeColumnName2); - var decimalColumn1 = columns.Single(x => x.Name == decimalColumnName1); - var guidColumn1 = columns.Single(x => x.Name == guidColumnName1); - var booleanColumn1 = columns.Single(x => x.Name == booleanColumnName1); - var int32Column1 = columns.Single(x => x.Name == int32ColumnName1); - var int64Column1 = columns.Single(x => x.Name == int64ColumnName1); - var int64Column2 = columns.Single(x => x.Name == int64ColumnName2); - var stringColumn1 = columns.Single(x => x.Name == stringColumnName1); - var binarycolumn1 = columns.Single(x => x.Name == binaryColumnName1); - var doubleColumn1 = columns.Single(x => x.Name == doubleColumnName1); - var intervalColumn1 = columns.Single(x => x.Name == intervalColumnName1); - var intervalColumn2 = columns.Single(x => x.Name == intervalColumnName2); + var dateTimeColumn1 = columns.Single(x => x.Name.Equals(dateTimeColumnName1, StringComparison.OrdinalIgnoreCase)); + var dateTimeColumn2 = columns.Single(x => x.Name.Equals(dateTimeColumnName2, StringComparison.OrdinalIgnoreCase)); + var decimalColumn1 = columns.Single(x => x.Name.Equals(decimalColumnName1, StringComparison.OrdinalIgnoreCase)); + var guidColumn1 = columns.Single(x => x.Name.Equals(guidColumnName1, StringComparison.OrdinalIgnoreCase)); + var booleanColumn1 = columns.Single(x => x.Name.Equals(booleanColumnName1, StringComparison.OrdinalIgnoreCase)); + var int32Column1 = columns.Single(x => x.Name.Equals(int32ColumnName1, StringComparison.OrdinalIgnoreCase)); + var int64Column1 = columns.Single(x => x.Name.Equals(int64ColumnName1, StringComparison.OrdinalIgnoreCase)); + var int64Column2 = columns.Single(x => x.Name.Equals(int64ColumnName2, StringComparison.OrdinalIgnoreCase)); + var stringColumn1 = columns.Single(x => x.Name.Equals(stringColumnName1, StringComparison.OrdinalIgnoreCase)); + var binarycolumn1 = columns.Single(x => x.Name.Equals(binaryColumnName1, StringComparison.OrdinalIgnoreCase)); + var doubleColumn1 = columns.Single(x => x.Name.Equals(doubleColumnName1, StringComparison.OrdinalIgnoreCase)); Assert.That(dateTimeColumn1.DefaultValue, Is.EqualTo(dateTimeDefaultValue)); Assert.That(dateTimeColumn2.DefaultValue, Is.EqualTo(dateTimeDefaultValue)); - Assert.That(decimalColumn1.DefaultValue, Is.EqualTo(DecimalDefaultValue)); + Assert.That(decimalColumn1.DefaultValue, Is.EqualTo(decimalDefaultValue)); Assert.That(guidColumn1.DefaultValue, Is.EqualTo(guidDefaultValue)); Assert.That(booleanColumn1.DefaultValue, Is.True); Assert.That(int32Column1.DefaultValue, Is.EqualTo(43)); Assert.That(int64Column1.DefaultValue, Is.EqualTo(88)); Assert.That(stringColumn1.DefaultValue, Is.EqualTo("Hello")); Assert.That(binarycolumn1.DefaultValue, Is.EqualTo(new byte[] { 12, 32, 34 })); - Assert.That(doubleColumn1.DefaultValue, Is.EqualTo(84.874596565)); - Assert.That(intervalColumn1.DefaultValue, Is.EqualTo(new TimeSpan(100000, 3, 4, 5, 666))); - Assert.That(intervalColumn2.DefaultValue, Is.EqualTo(new TimeSpan(0, 0, 0, 0, 666))); + Assert.That(doubleColumn1.DefaultValue, Is.EqualTo(84.874596567)); } // 1 will coerce to true on inserts but not for default values in Postgre SQL - same for 0 to false diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs new file mode 100644 index 00000000..92f8cb02 --- /dev/null +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_GetColumns_Tests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.PostgreSQL; + +[TestFixture] +[Category("Postgre")] +public class PostgreSQLTransformationProvider_GetColumns_Tests : TransformationProviderBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginPostgreSQLTransactionAsync(); + } +} diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyExistsTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyExistsTests.cs new file mode 100644 index 00000000..67ece8d7 --- /dev/null +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyExistsTests.cs @@ -0,0 +1,20 @@ +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.PostgreSQL; + +[TestFixture] +[Category("Postgre")] +public class PostgreSQLTransformationProvider_PrimaryKeyExistsTests : PostgreSQLTransformationProviderTestBase +{ + [Test] + public void CanAddPrimaryKey() + { + AddTable(); + AddPrimaryKey(); + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True); + } +} diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs index 1070bd93..204bad50 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_PrimaryKeyWithIdentityTests.cs @@ -1,6 +1,7 @@ using System; using System.Data; using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; using Npgsql; using NUnit.Framework; diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_TableExistsTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_TableExistsTests.cs index 38a80a21..fa53a604 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_TableExistsTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_TableExistsTests.cs @@ -1,5 +1,6 @@ using System.Data; using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; using NUnit.Framework; namespace Migrator.Tests.Providers.PostgreSQL; diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ViewExistsTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ViewExistsTests.cs index 0c57217f..b34dbcc2 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ViewExistsTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_ViewExistsTests.cs @@ -1,5 +1,6 @@ using System.Data; using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.PostgreSQL.Base; using NUnit.Framework; namespace Migrator.Tests.Providers.PostgreSQL; diff --git a/src/Migrator.Tests/Providers/SQLServer/Base/SQLServerTransformationProviderTestBase.cs b/src/Migrator.Tests/Providers/SQLServer/Base/SQLServerTransformationProviderTestBase.cs index 7f9cbe32..2c76169a 100644 --- a/src/Migrator.Tests/Providers/SQLServer/Base/SQLServerTransformationProviderTestBase.cs +++ b/src/Migrator.Tests/Providers/SQLServer/Base/SQLServerTransformationProviderTestBase.cs @@ -1,14 +1,5 @@ -using System.Threading; using System.Threading.Tasks; -using DotNetProjects.Migrator.Providers; -using DotNetProjects.Migrator.Providers.Impl.SqlServer; -using DryIoc; -using Migrator.Tests.Database; -using Migrator.Tests.Database.Interfaces; using Migrator.Tests.Providers.Base; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; -using Migrator.Tests.Settings.Models; using NUnit.Framework; namespace Migrator.Tests.Providers.SQLServer.Base; @@ -20,26 +11,7 @@ public abstract class SQLServerTransformationProviderTestBase : TransformationPr [SetUp] public async Task SetUpAsync() { - var configReader = new ConfigurationReader(); - - var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.SQLServerId); - - var connectionString = databaseConnectionConfig?.ConnectionString; - - if (string.IsNullOrEmpty(connectionString)) - { - throw new IgnoreException($"No SQL Server {nameof(DatabaseConnectionConfig.ConnectionString)} is set."); - } - - DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", () => Microsoft.Data.SqlClient.SqlClientFactory.Instance); - - using var container = new Container(); - container.RegisterDatabaseIntegrationTestService(); - var databaseIntegrationTestServiceFactory = container.Resolve(); - var sqlServerIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.SQLServer); - var databaseInfo = await sqlServerIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, CancellationToken.None); - - Provider = new SqlServerTransformationProvider(new SqlServerDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, "dbo", "default", "Microsoft.Data.SqlClient"); + await BeginSQLServerTransactionAsync(); AddDefaultTable(); } diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs new file mode 100644 index 00000000..3ed92ecc --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddTableTests.cs @@ -0,0 +1,30 @@ +using System.Data; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLServer; + +[TestFixture] +[Category("SqlServer")] +public class SQLServerTransformationProvider_AddTableTests : TransformationProviderBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLServerTransactionAsync(); + } + + [Test] + public void AddTableWithCompoundPrimaryKey() + { + Provider.AddTable("Test", + new Column("PersonId", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("AddressId", DbType.Int32, ColumnProperty.PrimaryKey) + ); + + Assert.That(Provider.TableExists("Test"), Is.True, "Table doesn't exist"); + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True, "Constraint doesn't exist"); + } +} diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs new file mode 100644 index 00000000..191a7603 --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_GetColumnsTests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLServer; + +[TestFixture] +[Category("SqlServer")] +public class SQLServerTransformationProvider_GetColumnsTests : TransformationProviderBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLServerTransactionAsync(); + } +} diff --git a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs index e574d275..f89967d6 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SqlServerTransformationProviderGenericTests.cs @@ -1,47 +1,20 @@ -using System; using System.Data; -using System.Threading; using System.Threading.Tasks; using DotNetProjects.Migrator.Providers; using DotNetProjects.Migrator.Providers.Impl.SqlServer; -using DryIoc; -using Migrator.Tests.Database; -using Migrator.Tests.Database.Interfaces; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; -using Migrator.Tests.Settings.Models; +using Migrator.Tests.Providers.Generic; using NUnit.Framework; namespace Migrator.Tests.Providers.SQLServer; [TestFixture] [Category("SqlServer")] -public class SqlServerTransformationProviderGenericTests : TransformationProviderConstraintBase +public class SqlServerTransformationProviderGenericTests : TransformationProviderGenericMiscConstraintBase { [SetUp] public async Task SetUpAsync() { - var configReader = new ConfigurationReader(); - - var databaseConnectionConfig = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.SQLServerId); - - var connectionString = databaseConnectionConfig?.ConnectionString; - - if (string.IsNullOrEmpty(connectionString)) - { - throw new IgnoreException($"No SQL Server {nameof(DatabaseConnectionConfig.ConnectionString)} is set."); - } - - DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", () => Microsoft.Data.SqlClient.SqlClientFactory.Instance); - - using var container = new Container(); - container.RegisterDatabaseIntegrationTestService(); - var databaseIntegrationTestServiceFactory = container.Resolve(); - var sqlServerIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.SQLServer); - var databaseInfo = await sqlServerIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, CancellationToken.None); - - Provider = new SqlServerTransformationProvider(new SqlServerDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, "dbo", "default", "Microsoft.Data.SqlClient"); - Provider.BeginTransaction(); + await BeginSQLServerTransactionAsync(); AddDefaultTable(); } diff --git a/src/Migrator.Tests/Providers/SQLite/Base/SQLiteTransformationProviderTestBase.cs b/src/Migrator.Tests/Providers/SQLite/Base/SQLiteTransformationProviderTestBase.cs index 69cd6a1a..995b0c28 100644 --- a/src/Migrator.Tests/Providers/SQLite/Base/SQLiteTransformationProviderTestBase.cs +++ b/src/Migrator.Tests/Providers/SQLite/Base/SQLiteTransformationProviderTestBase.cs @@ -1,7 +1,5 @@ -using DotNetProjects.Migrator.Providers.Impl.SQLite; +using System.Threading.Tasks; using Migrator.Tests.Providers.Base; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; using NUnit.Framework; namespace Migrator.Tests.Providers.SQLite.Base; @@ -11,15 +9,9 @@ namespace Migrator.Tests.Providers.SQLite.Base; public abstract class SQLiteTransformationProviderTestBase : TransformationProviderSimpleBase { [SetUp] - public void SetUp() + public async Task SetUpAsync() { - var configReader = new ConfigurationReader(); - var connectionString = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.SQLiteId) - .ConnectionString; - - Provider = new SQLiteTransformationProvider(new SQLiteDialect(), connectionString, "default", null); - Provider.BeginTransaction(); - + await BeginSQLiteTransactionAsync(); AddDefaultTable(); } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderGenericTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderGenericTests.cs index af56f8cd..2a7ba7ba 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderGenericTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProviderGenericTests.cs @@ -1,23 +1,17 @@ -using DotNetProjects.Migrator.Providers.Impl.SQLite; -using Migrator.Tests.Settings; -using Migrator.Tests.Settings.Config; +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; using NUnit.Framework; -namespace Migrator.Tests.Providers.SQLite.Base; +namespace Migrator.Tests.Providers.SQLite; [TestFixture] [Category("SQLite")] -public class SQLiteTransformationProviderGenericTests : TransformationProviderBase +public class SQLiteTransformationProviderGenericTests : TransformationProviderGenericMiscConstraintBase { [SetUp] - public void SetUp() + public async Task SetUpAsync() { - var configReader = new ConfigurationReader(); - var connectionString = configReader.GetDatabaseConnectionConfigById(DatabaseConnectionConfigIds.SQLiteId) - .ConnectionString; - - Provider = new SQLiteTransformationProvider(new SQLiteDialect(), connectionString, "default", null); - Provider.BeginTransaction(); + await BeginSQLiteTransactionAsync(); AddDefaultTable(); } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index 291d30c0..50fad73a 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -25,7 +25,9 @@ public void AddTable_UniqueOnly_ContainsNull() Assert.That("CREATE TABLE MyTableName (MyColumnName INTEGER NULL UNIQUE)", Is.EqualTo(createScript)); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); - Assert.That(sqliteInfo.Uniques.Single().KeyColumns.Single(), Is.EqualTo(columnName)); + + // It is no named unique so it is not listed in the Uniques list. Unique on column level is marked as obsolete. + Assert.That(sqliteInfo.Uniques, Is.Empty); } [Test] diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs new file mode 100644 index 00000000..c9e20eca --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetCheckConstraintsTests.cs @@ -0,0 +1,44 @@ +using System.Data.SQLite; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.SQLite; +using Migrator.Tests.Providers.SQLite.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLite; + +[TestFixture] +[Category("SQLite")] +public class SQLiteTransformationProvider_GetCheckConstraintsTests : SQLiteTransformationProviderTestBase +{ + [Test] + public void GetCheckConstraints_AddCheckConstraintsViaAddTable_CreatesTableCorrectly() + { + const string tableName = "MyTableName"; + const string columnName = "MyColumnName"; + const string checkConstraint1 = "MyCheckConstraint1"; + const string checkConstraint2 = "MyCheckConstraint2"; + + // Arrange/Act + Provider.AddTable(tableName, + new Column(columnName, System.Data.DbType.Int32), + new CheckConstraint(checkConstraint1, $"{columnName} > 10"), + new CheckConstraint(checkConstraint2, $"{columnName} < 100") + ); + + var checkConstraints = ((SQLiteTransformationProvider)Provider).GetCheckConstraints(tableName); + + // Assert + Assert.That(checkConstraints[0].Name, Is.EqualTo(checkConstraint1)); + Assert.That(checkConstraints[0].CheckConstraintString, Is.EqualTo($"{columnName} > 10")); + + Assert.That(checkConstraints[1].Name, Is.EqualTo(checkConstraint2)); + Assert.That(checkConstraints[1].CheckConstraintString, Is.EqualTo($"{columnName} < 100")); + + Provider.Insert(tableName, [columnName], [11]); + Assert.Throws(() => Provider.Insert(tableName, [columnName], [1])); + Assert.Throws(() => Provider.Insert(tableName, [columnName], [200])); + + var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); + Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (MyColumnName INTEGER NULL, CONSTRAINT MyCheckConstraint1 CHECK (MyColumnName > 10), CONSTRAINT MyCheckConstraint2 CHECK (MyColumnName < 100))")); + } +} diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs index da14ce25..3e7a048e 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs @@ -1,15 +1,22 @@ using System.Linq; +using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.SQLite; -using Migrator.Tests.Providers.SQLite.Base; +using Migrator.Tests.Providers.Base; using NUnit.Framework; namespace Migrator.Tests.Providers.SQLite; [TestFixture] [Category("SQLite")] -public class SQLiteTransformationProvider_GetColumnsTests : SQLiteTransformationProviderTestBase +public class SQLiteTransformationProvider_GetColumnsTests : TransformationProviderBase { + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLiteTransactionAsync(); + } + [Test] public void GetColumns_UniqueButNotPrimaryKey_ReturnsFalse() { @@ -94,7 +101,7 @@ public void GetColumns_AddUniqueWithTwoColumns_NoUniqueOnColumnLevel() Assert.That(columns[0].ColumnProperty, Is.EqualTo(ColumnProperty.Null)); } - [Test, Description("Add index. Should be added and detected as index")] + [Test, Description("Add index. The index should be added and then being detected as index.")] public void GetSQLiteTableInfo_GetIndexesAndColumnsWithIndex_NoUniqueOnTheColumnsAndIndexExists() { // Arrange diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs index 9c089e1c..5fd05fa4 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RemoveColumnTests.cs @@ -198,8 +198,9 @@ public void RemoveColumn_HavingMultipleSingleUniques_Succeeds() Provider.RemoveColumn(testTableName, propertyName2); var tableInfoAfter = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(testTableName); - Assert.That(tableInfoBefore.Uniques.Count, Is.EqualTo(2)); - Assert.That(tableInfoAfter.Uniques.Count, Is.EqualTo(1)); + // We do not support not named uniques in SQLite any more. + Assert.That(tableInfoBefore.Uniques.Count, Is.EqualTo(0)); + Assert.That(tableInfoAfter.Uniques.Count, Is.EqualTo(0)); } [Test] @@ -265,4 +266,23 @@ public void RemoveColumn_HavingAForeignKeyPointingFromTableToParentAndForeignKey var valid = ((SQLiteTransformationProvider)Provider).CheckForeignKeyIntegrity(); Assert.That(valid, Is.True); } + + [Test] + public void RemoveColumn_ColumnExistsInCheckConstraintString_Throws() + { + const string tableName = "MyTableName"; + const string columnName = "MyColumnName"; + const string checkConstraint1 = "MyCheckConstraint1"; + + // Arrange + Provider.AddTable(tableName, + new Column(columnName, System.Data.DbType.Int32), + new CheckConstraint(checkConstraint1, $"{columnName} > 10") + ); + + var checkConstraints = ((SQLiteTransformationProvider)Provider).GetCheckConstraints(tableName); + + // Act/Assert + Assert.Throws(() => Provider.RemoveColumn(tableName, columnName)); + } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs index 8821031d..6796ac30 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_RenameColumnTests.cs @@ -1,8 +1,8 @@ -using System; using System.Data; using System.Linq; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.SQLite; +using Migrator.Tests.Providers.Base; using Migrator.Tests.Providers.SQLite.Base; using NUnit.Framework; diff --git a/src/Migrator/Framework/CheckConstraint.cs b/src/Migrator/Framework/CheckConstraint.cs new file mode 100644 index 00000000..d682fa9a --- /dev/null +++ b/src/Migrator/Framework/CheckConstraint.cs @@ -0,0 +1,26 @@ +namespace DotNetProjects.Migrator.Framework; + +/// +/// Currently only used for SQLite +/// +public class CheckConstraint : IDbField +{ + public CheckConstraint() + { } + + public CheckConstraint(string name, string checkConstraintText) + { + CheckConstraintString = checkConstraintText; + Name = name; + } + + /// + /// Gets or sets the CheckConstraintString. Add it without the braces they will be added by the migrator. + /// + public string CheckConstraintString { get; set; } + + /// + /// Gets or sets the name of the CHECK constraint. + /// + public string Name { get; set; } +} diff --git a/src/Migrator/Framework/ColumnProperty.cs b/src/Migrator/Framework/ColumnProperty.cs index a49ddd1b..c89297e0 100644 --- a/src/Migrator/Framework/ColumnProperty.cs +++ b/src/Migrator/Framework/ColumnProperty.cs @@ -34,6 +34,7 @@ public enum ColumnProperty /// /// Indexed Column /// + [Obsolete("Use method 'AddIndex'")] Indexed = 1 << 4, /// diff --git a/src/Migrator/Framework/Extensions/LinqExtensions.cs b/src/Migrator/Framework/Extensions/LinqExtensions.cs new file mode 100644 index 00000000..d8bdcb4d --- /dev/null +++ b/src/Migrator/Framework/Extensions/LinqExtensions.cs @@ -0,0 +1,18 @@ +using System; + +namespace DotNetProjects.Migrator.Framework.Extensions; + +public static class LinqExtensions +{ + /// + /// Is equal to the Contains method in .NET 9. Please remove it after .NET upgrade. + /// + /// + /// + /// + /// + public static bool Contains(this string source, string toBeChecked, StringComparison stringComparison) + { + return source?.IndexOf(toBeChecked, stringComparison) >= 0; + } +} \ No newline at end of file diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index ac15af49..557426e7 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -349,8 +349,13 @@ public virtual string Default(object defaultValue) return guidValue; } - else if (defaultValue is DateTime) + else if (defaultValue is DateTime dateTime) { + if (dateTime.Kind != DateTimeKind.Utc) + { + throw new Exception("Use DateTimeKind.Utc for default date time values."); + } + return string.Format("DEFAULT '{0}'", ((DateTime)defaultValue).ToString("yyyy-MM-dd HH:mm:ss")); } else if (defaultValue is string) @@ -366,7 +371,7 @@ public virtual string Default(object defaultValue) else if (defaultValue is byte[] byteArray) { var convertedString = BitConverter.ToString(byteArray).Replace("-", "").ToLower(); - defaultValue = $"'\\x{convertedString}'"; + defaultValue = $"0x{convertedString}"; } else if (defaultValue is double doubleValue) { diff --git a/src/Migrator/Providers/Impl/Oracle/Models/UserTabColumns.cs b/src/Migrator/Providers/Impl/Oracle/Models/UserTabColumns.cs new file mode 100644 index 00000000..413a3d79 --- /dev/null +++ b/src/Migrator/Providers/Impl/Oracle/Models/UserTabColumns.cs @@ -0,0 +1,7 @@ +namespace DotNetProjects.Migrator.Providers.Impl.Oracle.Models; + +public class UserTabColumns +{ + public string ColumnName { get; set; } + public string DataDefault { get; set; } +} \ No newline at end of file diff --git a/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs b/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs index cb51faf3..7c3a20ad 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleDialect.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Linq; using DotNetProjects.Migrator.Framework; namespace DotNetProjects.Migrator.Providers.Impl.Oracle; @@ -16,6 +17,8 @@ public OracleDialect() RegisterColumnType(DbType.Binary, "RAW(2000)"); RegisterColumnType(DbType.Binary, 2000, "RAW($l)"); RegisterColumnType(DbType.Binary, 2147483647, "BLOB"); + + // 23ai now has a native boolean data type but for backwards compatibility we keep using NUMBER(1,0) RegisterColumnType(DbType.Boolean, "NUMBER(1,0)"); RegisterColumnType(DbType.Byte, "NUMBER(3,0)"); RegisterColumnType(DbType.Currency, "NUMBER(19,1)"); @@ -37,6 +40,7 @@ public OracleDialect() RegisterColumnType(DbType.UInt32, "NUMBER(10,0)"); RegisterColumnType(DbType.UInt64, "NUMBER(20,0)"); RegisterColumnType(DbType.Single, "FLOAT(24)"); + RegisterColumnType(DbType.Double, "BINARY_DOUBLE"); RegisterColumnType(DbType.StringFixedLength, "NCHAR(255)"); RegisterColumnType(DbType.StringFixedLength, 2000, "NCHAR($l)"); RegisterColumnType(DbType.String, "NVARCHAR2(255)"); @@ -118,17 +122,57 @@ public override ColumnPropertiesMapper GetColumnMapper(Column column) public override string Default(object defaultValue) { - if (defaultValue.GetType().Equals(typeof(bool))) + if (defaultValue == null) + { + return string.Empty; + } + + if (defaultValue is bool booleanValue) + { + return string.Format("DEFAULT {0}", booleanValue ? "1" : "0"); + } + else if (defaultValue is Guid guid) + { + var bytes = guid.ToByteArray(); + + // Convert to big-endian format in Oracle + var oracleBytes = new byte[16]; + + // Reverse first 4 bytes + Array.Copy(bytes, 0, oracleBytes, 0, 4); + Array.Reverse(oracleBytes, 0, 4); + + // Reverse next 2 bytes + Array.Copy(bytes, 4, oracleBytes, 4, 2); + Array.Reverse(oracleBytes, 4, 2); + + // Reverse next 2 bytes + Array.Copy(bytes, 6, oracleBytes, 6, 2); + Array.Reverse(oracleBytes, 6, 2); + + // Copy remaining 8bytes + Array.Copy(bytes, 8, oracleBytes, 8, 8); + + // Convert to hex string + var hex = BitConverter.ToString(oracleBytes).Replace("-", ""); + + return $"DEFAULT HEXTORAW('{hex}')"; + } + else if (defaultValue is DateTime dateTime) { - return string.Format("DEFAULT {0}", (bool)defaultValue ? "1" : "0"); + // We use 4 because we have no access data type and therefore no access to the real n in TIMESTAMP(n) in this method. Needs refactoring. + var dateTimeString = dateTime.ToString("yyyy-MM-dd HH:mm:ss.ffff"); + return $"DEFAULT TO_TIMESTAMP('{dateTimeString}', 'YYYY-MM-DD HH24:MI:SS.FF4')"; } - else if (defaultValue is Guid) + else if (defaultValue is string stringValue) { - return string.Format("DEFAULT HEXTORAW('{0}')", defaultValue.ToString().Replace("-", "")); + stringValue = stringValue.Replace("'", "''"); + return $"DEFAULT '{stringValue}'"; } - else if (defaultValue is DateTime) + else if (defaultValue is byte[] byteArray) { - return string.Format("DEFAULT TO_TIMESTAMP('{0}', 'YYYY-MM-DD HH24:MI:SS.FF')", ((DateTime)defaultValue).ToString("yyyy-MM-dd HH:mm:ss.ff")); + var convertedString = BitConverter.ToString(byteArray).Replace("-", "").ToLower(); + return $"DEFAULT HEXTORAW('{convertedString}')"; } return base.Default(defaultValue); diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index f8795cff..337250d1 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -1,11 +1,14 @@ using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Impl.Oracle.Models; using DotNetProjects.Migrator.Providers.Models; using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Text; +using System.Text.RegularExpressions; using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; using Index = DotNetProjects.Migrator.Framework.Index; @@ -384,114 +387,287 @@ public override string[] GetTables() public override Column[] GetColumns(string table) { + var timestampRegex = new Regex(@"(?<=^TIMESTAMP\s+')[^']+(?=')", RegexOptions.IgnoreCase); + var hexToRawRegex = new Regex(@"(?<=^HEXTORAW\s*\(')[^']+(?=')", RegexOptions.IgnoreCase); + var timestampBaseFormat = "yyyy-MM-dd HH:mm:ss"; + + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("SELECT"); + stringBuilder.AppendLine(" COLUMN_NAME,"); + stringBuilder.AppendLine(" NULLABLE,"); + stringBuilder.AppendLine(" DATA_DEFAULT,"); + stringBuilder.AppendLine(" DATA_TYPE,"); + stringBuilder.AppendLine(" DATA_LENGTH,"); + stringBuilder.AppendLine(" DATA_PRECISION,"); + stringBuilder.AppendLine(" DATA_SCALE,"); + stringBuilder.AppendLine(" CHAR_COL_DECL_LENGTH"); + stringBuilder.AppendLine($"FROM USER_TAB_COLUMNS WHERE LOWER(TABLE_NAME) = LOWER('{table}')"); + + var stringBuilder2 = new StringBuilder(); + stringBuilder2.AppendLine("SELECT x.column_name, x.data_default"); + stringBuilder2.AppendLine("FROM XMLTABLE("); + stringBuilder2.AppendLine(" '/ROWSET/ROW'"); + stringBuilder2.AppendLine(" PASSING DBMS_XMLGEN.GETXMLTYPE("); + stringBuilder2.AppendLine($" 'SELECT column_name, data_default FROM user_tab_columns WHERE table_name = ''{table.ToUpperInvariant()}'''"); + stringBuilder2.AppendLine(" )"); + stringBuilder2.AppendLine(" COLUMNS"); + stringBuilder2.AppendLine(" column_name VARCHAR2(4000) PATH 'COLUMN_NAME',"); + stringBuilder2.AppendLine(" data_default VARCHAR2(4000) PATH 'DATA_DEFAULT'"); + stringBuilder2.AppendLine(") x"); + + List userTabColumns = []; + + using (var cmd = CreateCommand()) + using (var reader = ExecuteQuery(cmd, stringBuilder2.ToString())) + { + while (reader.Read()) + { + var columnNameOrdinal = reader.GetOrdinal("COLUMN_NAME"); + var dataDefaultOrdinal = reader.GetOrdinal("DATA_DEFAULT"); + + var userTabColumnsItem = new UserTabColumns + { + ColumnName = reader.IsDBNull(columnNameOrdinal) ? null : reader.GetString(columnNameOrdinal), + DataDefault = reader.IsDBNull(dataDefaultOrdinal) ? null : reader.GetString(dataDefaultOrdinal).Trim() + }; + + userTabColumns.Add(userTabColumnsItem); + } + } + var columns = new List(); using (var cmd = CreateCommand()) - using ( - var reader = - ExecuteQuery(cmd, - string.Format( - "select column_name, data_type, data_length, data_precision, data_scale, NULLABLE, data_default FROM USER_TAB_COLUMNS WHERE lower(table_name) = '{0}'", - table.ToLower()))) + using (var reader = ExecuteQuery(cmd, stringBuilder.ToString())) { while (reader.Read()) { - var colName = reader[0].ToString(); - var colType = DbType.String; - var dataType = reader[1].ToString().ToLower(); - var isNullable = ParseBoolean(reader.GetValue(5)); - var defaultValue = reader.GetValue(6); + var columnNameOrdinal = reader.GetOrdinal("COLUMN_NAME"); + var nullableOrdinal = reader.GetOrdinal("NULLABLE"); + var dataTypeOrdinal = reader.GetOrdinal("DATA_TYPE"); + var dataLengthOrdinal = reader.GetOrdinal("DATA_LENGTH"); + var dataPrecisionOrdinal = reader.GetOrdinal("DATA_PRECISION"); + var dataScaleOrdinal = reader.GetOrdinal("DATA_SCALE"); + var charColDeclLengthOrdinal = reader.GetOrdinal("CHAR_COL_DECL_LENGTH"); + + var columnName = reader.GetString(columnNameOrdinal); + var isNullable = reader.GetString(nullableOrdinal) == "Y"; + var dataTypeString = reader.GetString(dataTypeOrdinal).ToUpperInvariant(); + var dataLength = reader.IsDBNull(dataLengthOrdinal) ? (int?)null : reader.GetInt32(dataLengthOrdinal); + var dataPrecision = reader.IsDBNull(dataPrecisionOrdinal) ? (int?)null : reader.GetInt32(dataPrecisionOrdinal); + var dataScale = reader.IsDBNull(dataScaleOrdinal) ? (int?)null : reader.GetInt32(dataScaleOrdinal); + var charColDeclLength = reader.IsDBNull(charColDeclLengthOrdinal) ? (int?)null : reader.GetInt32(charColDeclLengthOrdinal); + var dataDefaultString = userTabColumns.FirstOrDefault(x => x.ColumnName.Equals(columnName, StringComparison.OrdinalIgnoreCase))?.DataDefault; + + var column = new Column(columnName, DbType.String) + { + ColumnProperty = isNullable ? ColumnProperty.Null : ColumnProperty.NotNull + }; - if (dataType.Equals("number")) + // Oracle does not have unsigned types. All NUMBER types can hold positive or negative values so we do not return DbType.UIntX types. + if (dataTypeString.StartsWith("NUMBER") || dataTypeString.StartsWith("FLOAT")) { - var precision = Convert.ToInt32(reader.GetValue(3)); - var scale = Convert.ToInt32(reader.GetValue(4)); - if (scale == 0) + column.Precision = dataPrecision; + + if (dataScale > 0) { - colType = precision <= 10 ? DbType.Int16 : DbType.Int64; + // Could also be Double + column.MigratorDbType = MigratorDbType.Decimal; + column.Scale = dataScale; } else { - colType = DbType.Decimal; + if (dataPrecision.HasValue && dataPrecision == 1) + { + column.MigratorDbType = MigratorDbType.Boolean; + } + else if (dataPrecision.HasValue && (dataPrecision == 0 || (2 <= dataPrecision && dataPrecision <= 5))) + { + column.MigratorDbType = MigratorDbType.Int16; + } + else if (dataPrecision.HasValue && 6 <= dataPrecision && dataPrecision <= 10) + { + column.MigratorDbType = MigratorDbType.Int32; + } + else if (dataPrecision == null || 11 <= dataPrecision) + { + // Oracle allows up to 38 digits but in C# the maximum is Int64 and in Oracle there is no unsigned data type. + column.MigratorDbType = MigratorDbType.Int64; + } + else + { + throw new NotSupportedException(); + } } } - else if (dataType.StartsWith("timestamp") || dataType.Equals("date")) + else if (dataTypeString.StartsWith("TIMESTAMP")) { - colType = DbType.DateTime; - } - - var columnProperties = (isNullable) ? ColumnProperty.Null : ColumnProperty.NotNull; - var column = new Column(colName, colType, columnProperties); + var timestampNumberRegex = new Regex(@"(?<=^Timestamp\()[\d]+(?=\)$)", RegexOptions.IgnoreCase); + var timestampNumberMatch = timestampNumberRegex.Match(dataTypeString); - if (defaultValue != null && defaultValue != DBNull.Value) + if (timestampNumberMatch.Success) + { + // n in TIMESTAMP(n) is not retrievable using system tables so we need to extract it via regex. + column.Precision = int.Parse(timestampNumberMatch.Value); + column.MigratorDbType = column.Precision < 3 ? MigratorDbType.DateTime : MigratorDbType.DateTime2; + } + else + { + // 6 is the standard if we use TIMESTAMP without n like in TIMESTAMP(n) + column.Precision = 6; + column.MigratorDbType = MigratorDbType.DateTime2; + } + } + else if (dataTypeString == "DATE") { - column.DefaultValue = defaultValue; + column.MigratorDbType = MigratorDbType.Date; } - - if (column.DefaultValue is string && ((string)column.DefaultValue).StartsWith("'") && ((string)column.DefaultValue).EndsWith("'")) + else if (dataTypeString == "RAW" && dataLength == 16) + { + // ambiguity - cannot distinguish between guid and binary + column.MigratorDbType = MigratorDbType.Guid; + } + else if (dataTypeString.StartsWith("RAW") || dataTypeString == "BLOB") + { + column.MigratorDbType = MigratorDbType.Binary; + } + else if (dataTypeString == "NVARCHAR2") + { + column.MigratorDbType = MigratorDbType.String; + } + else if (dataTypeString == "BINARY_FLOAT") + { + column.MigratorDbType = MigratorDbType.Single; + } + else if (dataTypeString == "BINARY_DOUBLE") + { + column.MigratorDbType = MigratorDbType.Double; + } + else if (dataTypeString == "BOOLEAN") + { + column.MigratorDbType = MigratorDbType.Boolean; + } + else if (dataTypeString == "NCLOB") { - column.DefaultValue = ((string)column.DefaultValue).Substring(1, ((string)column.DefaultValue).Length - 2); + column.MigratorDbType = MigratorDbType.String; + } + else + { + throw new NotImplementedException(); } - if ((column.DefaultValue is string s && !string.IsNullOrEmpty(s)) || - column.DefaultValue is not string && column.DefaultValue != null) + if (!string.IsNullOrWhiteSpace(dataDefaultString)) { if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) { - column.DefaultValue = long.Parse(column.DefaultValue.ToString()); + column.DefaultValue = long.Parse(dataDefaultString, CultureInfo.InvariantCulture); } - else if (column.Type == DbType.UInt16 || column.Type == DbType.UInt32 || column.Type == DbType.UInt64) + else if (column.Type == DbType.Double) { - column.DefaultValue = ulong.Parse(column.DefaultValue.ToString()); + column.DefaultValue = double.Parse(dataDefaultString, CultureInfo.InvariantCulture); } - else if (column.Type == DbType.Double || column.Type == DbType.Single) + else if (column.Type == DbType.Single) { - column.DefaultValue = double.Parse(column.DefaultValue.ToString()); + column.DefaultValue = float.Parse(dataDefaultString, CultureInfo.InvariantCulture); + } + else if (column.Type == DbType.Decimal) + { + column.DefaultValue = decimal.Parse(dataDefaultString, CultureInfo.InvariantCulture); } else if (column.Type == DbType.Boolean) { - column.DefaultValue = column.DefaultValue.ToString().Trim() == "1" || column.DefaultValue.ToString().Trim().ToUpper() == "TRUE"; + column.DefaultValue = dataDefaultString == "1" || dataDefaultString.ToUpper() == "TRUE"; } else if (column.Type == DbType.DateTime || column.Type == DbType.DateTime2) { - if (column.DefaultValue is string defValCv && defValCv.StartsWith("TO_TIMESTAMP(")) - { - var dt = defValCv.Substring((defValCv.IndexOf("'") + 1), defValCv.IndexOf("'", defValCv.IndexOf("'") + 1) - defValCv.IndexOf("'") - 1); - var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss.ff", CultureInfo.InvariantCulture); - column.DefaultValue = d; - } - else if (column.DefaultValue is string defVal) + if (dataDefaultString.StartsWith("TO_TIMESTAMP(")) { - var dt = defVal; - if (defVal.StartsWith("'")) + var expectedOracleToTimestampPattern = "YYYY-MM-DD HH24:MI:SS"; + + if (!dataDefaultString.Contains(expectedOracleToTimestampPattern)) { - dt = defVal.Substring(1, defVal.Length - 2); + throw new NotSupportedException($"Not supported 'TO_TIMESTAMP' pattern. Expected pattern: {expectedOracleToTimestampPattern}"); } - var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); - column.DefaultValue = d; + var toTimestampRegex = new Regex(@"(?<=^TO_TIMESTAMP\(')[^']+(?=')", RegexOptions.IgnoreCase); + var toTimestampMatch = toTimestampRegex.Match(dataDefaultString); + var toTimestampDateTimeString = toTimestampMatch.Value; + + List formats = []; + + // add formats with .F, .FF, .FFF etc. + formats = Enumerable.Range(0, 20).Select((x, y) => $"{timestampBaseFormat}.{new string('F', y + 1)}").ToList(); + formats.Add(timestampBaseFormat); + + column.DefaultValue = DateTime.ParseExact(toTimestampDateTimeString, [.. formats], CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); + } + else if (timestampRegex.Match(dataDefaultString) is Match timestampMatch && timestampMatch.Success) + { + var millisecondsPattern = column.Size == 0 ? string.Empty : $".{new string('F', column.Size)}"; + column.DefaultValue = DateTime.ParseExact(timestampMatch.Value, $"yyyy-MM-dd HH:mm:ss{millisecondsPattern}", CultureInfo.InvariantCulture); + } + else + { + // Could be system time in many variants + column.DefaultValue = dataDefaultString; } } else if (column.Type == DbType.Guid) { - if (column.DefaultValue is string defValCv && defValCv.StartsWith("HEXTORAW(")) + if (hexToRawRegex.Match(dataDefaultString) is Match hexToRawMatch && hexToRawMatch.Success) { - var dt = defValCv.Substring((defValCv.IndexOf("'") + 1), defValCv.IndexOf("'", defValCv.IndexOf("'") + 1) - defValCv.IndexOf("'") - 1); - var d = Guid.Parse(dt); - column.DefaultValue = d; + var bytes = Enumerable.Range(0, hexToRawMatch.Value.Length / 2) + .Select(x => Convert.ToByte(hexToRawMatch.Value.Substring(x * 2, 2), 16)) + .ToArray(); + + // Oracle uses Big-Endian + Array.Reverse(bytes, 0, 4); + Array.Reverse(bytes, 4, 2); + Array.Reverse(bytes, 6, 2); + + column.DefaultValue = new Guid(bytes); } - else if (column.DefaultValue is string defVal) + else if (dataDefaultString.StartsWith("'")) { - var dt = defVal; - if (defVal.StartsWith("'")) - { - dt = defVal.Substring(1, defVal.Length - 2); - } + var guidString = dataDefaultString.Substring(1, dataDefaultString.Length - 2); + + column.DefaultValue = Guid.Parse(guidString); + } + else + { + column.DefaultValue = dataDefaultString; + } + } + else if (column.Type == DbType.String) + { + var contentRegex = new Regex(@"(?<=^').*(?='$)"); - var d = Guid.Parse(dt); - column.DefaultValue = d; + if (contentRegex.Match(dataDefaultString) is Match contentMatch && contentMatch.Success) + { + column.DefaultValue = contentMatch.Value; + } + else + { + throw new Exception($"Cannot parse string column '{column.Name}'"); + } + } + else if (column.Type == DbType.Binary) + { + if (hexToRawRegex.Match(dataDefaultString) is Match hexToRawMatch && hexToRawMatch.Success) + { + column.DefaultValue = Enumerable.Range(0, hexToRawMatch.Value.Length / 2) + .Select(x => Convert.ToByte(hexToRawMatch.Value.Substring(x * 2, 2), 16)) + .ToArray(); + } + else + { + throw new NotImplementedException($"Cannot parse default value in column '{column.Name}'"); } } + else + { + column.DefaultValue = dataDefaultString; + } } columns.Add(column); @@ -501,24 +677,6 @@ public override Column[] GetColumns(string table) return columns.ToArray(); } - private bool ParseBoolean(object value) - { - if (value is string) - { - if ("N" == (string)value) - { - return false; - } - - if ("Y" == (string)value) - { - return true; - } - } - - return Convert.ToBoolean(value); - } - public override string GenerateParameterNameParameter(int index) { return "p" + index; diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs index f536745e..d6e8c2a7 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLDialect.cs @@ -121,6 +121,11 @@ public override string Default(object defaultValue) return $"DEFAULT '{intervalPostgreNotation}'"; } + else if (defaultValue is byte[] byteArray) + { + var convertedString = BitConverter.ToString(byteArray).Replace("-", "").ToLower(); + return @$"DEFAULT E'\\x{convertedString}'"; + } return base.Default(defaultValue); } diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index 2c7da7fa..34f04aee 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -470,7 +470,7 @@ public override Column[] GetColumns(string table) { // We assume that the value was added using this migrator so we do not interpret things like '2 days 01:02:03' if you // added such format you will run into this exception. - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}' unexpected pattern."); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}' unexpected pattern."); } } else if (column.MigratorDbType == MigratorDbType.Boolean) @@ -488,7 +488,7 @@ public override Column[] GetColumns(string table) } else { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } } else if (column.MigratorDbType == MigratorDbType.DateTime || column.MigratorDbType == MigratorDbType.DateTime2) @@ -499,7 +499,7 @@ public override Column[] GetColumns(string table) if (!match.Success) { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } var timeString = match.Value; @@ -511,7 +511,7 @@ public override Column[] GetColumns(string table) } else { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } } else if (column.MigratorDbType == MigratorDbType.Guid) @@ -522,14 +522,14 @@ public override Column[] GetColumns(string table) if (!match.Success) { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } column.DefaultValue = Guid.Parse(match.Value); } else { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } } else if (column.MigratorDbType == MigratorDbType.Decimal) @@ -562,7 +562,7 @@ public override Column[] GetColumns(string table) if (!match.Success) { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } var singleQuoteString = match.Value; @@ -582,7 +582,7 @@ public override Column[] GetColumns(string table) } else { - throw new NotImplementedException($"Cannot interpret {defaultValueString} in column '{column.Name}'"); + throw new NotImplementedException($"Cannot parse {defaultValueString} in column '{column.Name}'"); } } else diff --git a/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs b/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs index e2512c77..c839b034 100644 --- a/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs +++ b/src/Migrator/Providers/Impl/SQLite/Models/SQLiteTableInfo.cs @@ -34,4 +34,9 @@ public class SQLiteTableInfo /// Gets or sets the unique definitions. /// public List Uniques { get; set; } = []; + + /// + /// Gets or sets the check constraint definitions. + /// + public List CheckConstraints { get; set; } = []; } \ No newline at end of file diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 0eddfc56..fdb3a0be 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -9,6 +9,7 @@ using System.Text.RegularExpressions; using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; using Index = DotNetProjects.Migrator.Framework.Index; +using DotNetProjects.Migrator.Framework.Extensions; namespace DotNetProjects.Migrator.Providers.Impl.SQLite; @@ -108,7 +109,7 @@ public string GetSqlCreateTableScript(string table) { if (reader.Read()) { - sqlCreateTableScript = (string)reader[0]; + sqlCreateTableScript = reader.IsDBNull(0) ? null : (string)reader[0]; } } @@ -340,8 +341,20 @@ public DbType ExtractTypeFromColumnDef(string columnDef) public override void RemoveForeignKey(string table, string name) { - //Check the impl... - return; + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + var sqliteTableInfo = GetSQLiteTableInfo(table); + if (!sqliteTableInfo.ForeignKeys.Any(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException($"Foreign key '{name}' does not exist."); + } + + sqliteTableInfo.ForeignKeys.RemoveAll(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + + RecreateTable(sqliteTableInfo); } public string[] GetCreateIndexSqlStrings(string table) @@ -404,6 +417,13 @@ public override void RemoveColumn(string tableName, string column) var sqliteInfoMainTable = GetSQLiteTableInfo(tableName); + var checkConstraints = sqliteInfoMainTable.CheckConstraints; + + if (checkConstraints.Any(x => x.CheckConstraintString.Contains(column, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException("A check constraint contains the column you want to remove. Remove the check constraint first"); + } + if (!sqliteInfoMainTable.ColumnMappings.Any(x => x.OldName == column)) { throw new MigrationException("Column not found"); @@ -632,6 +652,11 @@ public override void AddPrimaryKey(string name, string tableName, params string[ RecreateTable(sqliteTableInfo); } + public override bool PrimaryKeyExists(string table, string name) + { + throw new NotSupportedException($"SQLite does not support named primary keys. You may wonder why there is a name in method '{nameof(AddPrimaryKey)}'. It is because of architectural decisions of the past. It is overridden in {nameof(SQLiteTransformationProvider)}."); + } + public override void AddUniqueConstraint(string name, string table, params string[] columns) { var sqliteTableInfo = GetSQLiteTableInfo(table); @@ -641,15 +666,30 @@ public override void AddUniqueConstraint(string name, string table, params strin RecreateTable(sqliteTableInfo); } + public override void RemoveConstraint(string table, string name) + { + var sqliteTableInfo = GetSQLiteTableInfo(table); + sqliteTableInfo.Uniques.RemoveAll(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + sqliteTableInfo.CheckConstraints.RemoveAll(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + + RecreateTable(sqliteTableInfo); + } + public SQLiteTableInfo GetSQLiteTableInfo(string tableName) { + if (!TableExists(tableName)) + { + return null; + } + var sqliteTable = new SQLiteTableInfo { TableNameMapping = new MappingInfo { OldName = tableName, NewName = tableName }, Columns = GetColumns(tableName).ToList(), ForeignKeys = GetForeignKeyConstraints(tableName).ToList(), Indexes = GetIndexes(tableName).ToList(), - Uniques = GetUniques(tableName).ToList() + Uniques = GetUniques(tableName).ToList(), + CheckConstraints = GetCheckConstraints(tableName) }; sqliteTable.ColumnMappings = sqliteTable.Columns @@ -707,9 +747,11 @@ public void RecreateTable(SQLiteTableInfo sqliteTableInfo) var foreignKeyDbFields = sqliteTableInfo.ForeignKeys.Cast(); var indexDbFields = sqliteTableInfo.Indexes.Cast(); var uniqueDbFields = sqliteTableInfo.Uniques.Cast(); + var checkConstraintDbFields = sqliteTableInfo.CheckConstraints.Cast(); var dbFields = columnDbFields.Concat(foreignKeyDbFields) .Concat(uniqueDbFields) + .Concat(checkConstraintDbFields) .ToArray(); // ToHashSet() not available in older .NET versions so we create it old-fashioned. @@ -770,6 +812,12 @@ public void RecreateTable(SQLiteTableInfo sqliteTableInfo) } } + [Obsolete] + public override void AddTable(string table, string engine, string columns) + { + throw new NotSupportedException(); + } + public override void AddColumn(string table, Column column) { if (!TableExists(table)) @@ -778,6 +826,7 @@ public override void AddColumn(string table, Column column) } var sqliteInfo = GetSQLiteTableInfo(table); + if (sqliteInfo.ColumnMappings.Select(x => x.OldName).ToList().Contains(column.Name)) { throw new Exception("Column already exists."); @@ -789,6 +838,83 @@ public override void AddColumn(string table, Column column) RecreateTable(sqliteInfo); } + public override void AddColumn(string table, string columnName, DbType type, int size) + { + var column = new Column(columnName, type, size); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type, int size) + { + var column = new Column(columnName, type, size); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type, ColumnProperty property) + { + var column = new Column(columnName, type, property); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type, ColumnProperty property) + { + var column = new Column(columnName, type, property); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type, int size, ColumnProperty property, + object defaultValue) + { + var column = new Column(columnName, type, property) { Size = size, DefaultValue = defaultValue }; + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type) + { + var column = new Column(columnName, type); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type) + { + var column = new Column(columnName, type); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type, int size, ColumnProperty property) + { + var column = new Column(columnName, type, size, property); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, MigratorDbType type, int size, ColumnProperty property) + { + var column = new Column(columnName, type, size, property); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string columnName, DbType type, object defaultValue) + { + var column = new Column(columnName, type, defaultValue); + + AddColumn(table, column); + } + + public override void AddColumn(string table, string sqlColumn) + { + var column = new Column(sqlColumn); + AddColumn(table, column); + } + public override void ChangeColumn(string table, Column column) { if (!TableExists(table)) @@ -840,6 +966,11 @@ public override List GetDatabases() public override bool ConstraintExists(string table, string name) { + if (!TableExists(table)) + { + throw new Exception($"Table '{table}' does not exist."); + } + var constraintNames = GetConstraints(table); var exists = constraintNames.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase)); @@ -849,6 +980,11 @@ public override bool ConstraintExists(string table, string name) public override string[] GetConstraints(string table) { + if (!TableExists(table)) + { + throw new Exception($"Table '{table}' does not exist."); + } + var sqliteInfo = GetSQLiteTableInfo(table); var foreignKeyNames = sqliteInfo.ForeignKeys @@ -859,9 +995,12 @@ public override string[] GetConstraints(string table) .Select(x => x.Name) .ToList(); - // TODO add PK and CHECK + var checkConstraints = sqliteInfo.CheckConstraints + .Select(x => x.Name) + .ToList(); var names = foreignKeyNames.Concat(uniqueConstraints) + .Concat(checkConstraints) .Where(x => !string.IsNullOrWhiteSpace(x)) .ToArray(); @@ -966,7 +1105,7 @@ public override Column[] GetColumns(string tableName) dt = defVal.Substring(1, defVal.Length - 2); } - var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); column.DefaultValue = d; } } @@ -985,6 +1124,10 @@ public override Column[] GetColumns(string tableName) column.DefaultValue = d; } } + else if (column.Type == DbType.Boolean) + { + throw new NotSupportedException("SQLite does not support default values for BLOB columns."); + } } if (pragmaTableInfoItem.Pk > 0) @@ -1119,6 +1262,8 @@ public override void AddTable(string name, string engine, params IDbField[] fiel stringBuilder.Append(string.Format(", PRIMARY KEY ({0})", string.Join(", ", pks.ToArray()))); } + + // Uniques var uniques = fields.Where(x => x is Unique).Cast().ToArray(); foreach (var u in uniques) @@ -1136,6 +1281,7 @@ public override void AddTable(string name, string engine, params IDbField[] fiel stringBuilder.Append($" UNIQUE ({uniqueColumnsCommaSeparated})"); } + // Foreign keys var foreignKeys = fields.Where(x => x is ForeignKeyConstraint).Cast().ToArray(); List foreignKeyStrings = []; @@ -1154,12 +1300,25 @@ public override void AddTable(string name, string engine, params IDbField[] fiel foreignKeyStrings.Add($"CONSTRAINT {fk.Name} FOREIGN KEY ({sourceColumnNamesQuotedString}) REFERENCES {parentTableNameQuoted}({parentColumnNamesQuotedString})"); } - if (foreignKeyStrings.Count != 0) + if (foreignKeyStrings.Count > 0) { stringBuilder.Append(", "); stringBuilder.Append(string.Join(", ", foreignKeyStrings)); } + // Check Constraints + var checkConstraints = fields.Where(x => x is CheckConstraint).OfType().ToArray(); + List checkConstraintStrings = []; + + foreach (var checkConstraint in checkConstraints) + { + checkConstraintStrings.Add($"CONSTRAINT {checkConstraint.Name} CHECK ({checkConstraint.CheckConstraintString})"); + } + + if (checkConstraintStrings.Count > 0) + { + stringBuilder.Append($", {string.Join(", ", checkConstraintStrings)}"); + } stringBuilder.Append(')'); @@ -1239,6 +1398,11 @@ public override void RemoveAllIndexes(string tableName) public List GetUniques(string tableName) { + if (!TableExists(tableName)) + { + throw new Exception($"Table '{tableName}' does not exist."); + } + var regEx = new Regex(@"(?<=,)\s*(CONSTRAINT\s+\w+\s+)?UNIQUE\s*\(\s*[\w\s,]+\s*\)\s*(?=,|\s*\))"); var regExConstraintName = new Regex(@"(?<=CONSTRAINT\s+)\w+(?=\s+)"); var regExParenthesis = new Regex(@"(?<=\().+(?=\))"); @@ -1273,10 +1437,20 @@ public List GetUniques(string tableName) var createScript = GetSqlCreateTableScript(tableName); - var matches = regEx.Matches(createScript).Cast().Where(x => x.Success).Select(x => x.Value.Trim()).ToList(); + var matches = regEx.Matches(createScript); + if (matches.Count == 0) + { + return []; + } + + var constraintNames = matches + .OfType() + .Where(x => x.Success && !string.IsNullOrWhiteSpace(x.Value)) + .Select(x => x.Value.Trim()) + .ToList(); // We can only use the ones containing a starting with CONSTRAINT - var matchesHavingName = matches.Where(x => x.StartsWith("CONSTRAINT")).ToList(); + var matchesHavingName = constraintNames.Where(x => x.StartsWith("CONSTRAINT")).ToList(); foreach (var constraintString in matchesHavingName) { @@ -1385,6 +1559,68 @@ public List GetPragmaTableInfoItems(string tableNameNotQuot return pragmaTableInfoItems; } + public override void AddCheckConstraint(string constraintName, string tableName, string checkSql) + { + var sqliteTableInfo = GetSQLiteTableInfo(tableName); + + var checkConstraint = new CheckConstraint(constraintName, checkSql); + sqliteTableInfo.CheckConstraints.Add(checkConstraint); + + RecreateTable(sqliteTableInfo); + } + + public List GetCheckConstraints(string tableName) + { + if (!TableExists(tableName)) + { + throw new Exception($"Table '{tableName}' does not exist."); + } + + var checkConstraintRegex = new Regex(@"(?<=,)[^,]+\s+[^,]+check[^,]+(?=[,|\)])", RegexOptions.IgnoreCase); + var braceContentRegex = new Regex(@"(?<=^\().+(?=\)$)"); + + var script = GetSqlCreateTableScript(tableName); + + var matches = checkConstraintRegex.Matches(script); + + if (matches == null) + { + return []; + } + + var checkStrings = matches.OfType() + .Where(x => x.Success) + .Select(x => x.Value) + .ToList(); + + List checkConstraints = []; + + foreach (var checkString in checkStrings) + { + var splitted = checkString.Trim().Split(' ') + .Select(x => x.Trim()) + .ToList(); + + if (!splitted[0].Equals("CONSTRAINT", StringComparison.OrdinalIgnoreCase) || !splitted[2].Equals("CHECK", StringComparison.OrdinalIgnoreCase)) + { + throw new Exception($"Cannot parse check constraint in table {tableName}"); + } + + var checkConstraintStringWithBraces = string.Join(" ", splitted.Skip(3)).Trim(); + var checkConstraintString = braceContentRegex.Match(checkConstraintStringWithBraces); + + var checkConstraint = new CheckConstraint + { + Name = splitted[1], + CheckConstraintString = checkConstraintString.Value + }; + + checkConstraints.Add(checkConstraint); + } + + return checkConstraints; + } + protected override void ConfigureParameterWithValue(IDbDataParameter parameter, int index, object value) { if (value is ushort) diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 3cb2568d..e69ebc48 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -16,6 +16,8 @@ using System.Collections.Generic; using System.Data; using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; using Index = DotNetProjects.Migrator.Framework.Index; namespace DotNetProjects.Migrator.Providers.Impl.SqlServer; @@ -321,7 +323,7 @@ public override Column[] GetColumns(string table) var pkColumns = new List(); try { - pkColumns = this.ExecuteStringQuery("SELECT cu.COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE cu WHERE EXISTS ( SELECT tc.* FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc WHERE tc.TABLE_NAME = '{0}' AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.CONSTRAINT_NAME = cu.CONSTRAINT_NAME )", table); + pkColumns = ExecuteStringQuery("SELECT cu.COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE cu WHERE EXISTS ( SELECT tc.* FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc WHERE tc.TABLE_NAME = '{0}' AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.CONSTRAINT_NAME = cu.CONSTRAINT_NAME )", table); } catch (Exception) { } @@ -329,7 +331,7 @@ public override Column[] GetColumns(string table) var idtColumns = new List(); try { - idtColumns = this.ExecuteStringQuery(" select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA = '{1}' and TABLE_NAME = '{0}' and COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1", table, schema); + idtColumns = ExecuteStringQuery("SELECT COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA = '{1}' and TABLE_NAME = '{0}' and COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') = 1", table, schema); } catch (Exception) { } @@ -339,12 +341,19 @@ public override Column[] GetColumns(string table) using ( var reader = ExecuteQuery(cmd, - string.Format("select COLUMN_NAME, IS_NULLABLE, DATA_TYPE, ISNULL(CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION), COLUMN_DEFAULT, NUMERIC_SCALE from INFORMATION_SCHEMA.COLUMNS where table_name = '{0}'", table))) + string.Format("SELECT COLUMN_NAME, IS_NULLABLE, DATA_TYPE, ISNULL(CHARACTER_MAXIMUM_LENGTH , NUMERIC_PRECISION), COLUMN_DEFAULT, NUMERIC_SCALE, CHARACTER_MAXIMUM_LENGTH from INFORMATION_SCHEMA.COLUMNS where table_name = '{0}'", table))) { while (reader.Read()) { var column = new Column(reader.GetString(0), DbType.String); + var defaultValueOrdinal = reader.GetOrdinal("COLUMN_DEFAULT"); + var dataTypeOrdinal = reader.GetOrdinal("DATA_TYPE"); + var characterMaximumLengthOrdinal = reader.GetOrdinal("CHARACTER_MAXIMUM_LENGTH"); + + var defaultValueString = reader.IsDBNull(defaultValueOrdinal) ? null : reader.GetString(defaultValueOrdinal).Trim(); + var characterMaximumLength = reader.IsDBNull(characterMaximumLengthOrdinal) ? (int?)null : reader.GetInt32(characterMaximumLengthOrdinal); + if (pkColumns.Contains(column.Name)) { column.ColumnProperty |= ColumnProperty.PrimaryKey; @@ -357,77 +366,187 @@ public override Column[] GetColumns(string table) var nullableStr = reader.GetString(1); var isNullable = nullableStr == "YES"; - if (!reader.IsDBNull(2)) + + var dataTypeString = reader.GetString(dataTypeOrdinal); + + if (dataTypeString == "date") + { + column.MigratorDbType = MigratorDbType.Date; + } + else if (dataTypeString == "int") + { + column.MigratorDbType = MigratorDbType.Int32; + } + else if (dataTypeString == "bigint") + { + column.MigratorDbType = MigratorDbType.Int64; + } + else if (dataTypeString == "smallint") { - var type = reader.GetString(2); - column.Type = Dialect.GetDbTypeFromString(type); + column.MigratorDbType = MigratorDbType.Int16; } + else if (dataTypeString == "tinyint") + { + column.MigratorDbType = MigratorDbType.Byte; + } + else if (dataTypeString == "bit") + { + column.MigratorDbType = MigratorDbType.Boolean; + } + else if (dataTypeString == "money") + { + column.MigratorDbType = MigratorDbType.Currency; + } + else if (dataTypeString == "float") + { + column.MigratorDbType = MigratorDbType.Double; + } + else if (new[] { "text", "nchar", "ntext", "varchar", "nvarchar" }.Contains(dataTypeString)) + { + // We use string for all string-like data types. + column.MigratorDbType = MigratorDbType.String; + column.Size = characterMaximumLength.Value; + } + else if (dataTypeString == "decimal") + { + column.MigratorDbType = MigratorDbType.Decimal; + } + else if (dataTypeString == "datetime") + { + column.MigratorDbType = MigratorDbType.DateTime; + } + else if (dataTypeString == "datetime2") + { + column.MigratorDbType = MigratorDbType.DateTime2; + } + else if (dataTypeString == "datetimeoffset") + { + column.MigratorDbType = MigratorDbType.DateTimeOffset; + } + else if (dataTypeString == "binary" || dataTypeString == "varbinary") + { + column.MigratorDbType = MigratorDbType.Binary; + } + else if (dataTypeString == "uniqueidentifier") + { + column.MigratorDbType = MigratorDbType.Guid; + } + else if (dataTypeString == "real") + { + column.MigratorDbType = MigratorDbType.Single; + } + else + { + throw new NotImplementedException($"The data type '{dataTypeString}' is not implemented yet. Please file an issue."); + } + if (!reader.IsDBNull(3)) { column.Size = reader.GetInt32(3); } - if (!reader.IsDBNull(4)) - { - column.DefaultValue = reader.GetValue(4); - if (column.DefaultValue.ToString()[1] == '(' || column.DefaultValue.ToString()[1] == '\'') - { - column.DefaultValue = column.DefaultValue.ToString().Substring(2, column.DefaultValue.ToString().Length - 4); // Example "((10))" or "('false')" - } - else - { - column.DefaultValue = column.DefaultValue.ToString().Substring(1, column.DefaultValue.ToString().Length - 2); // Example "(CONVERT([datetime],'20000101',(112)))" - } + if (defaultValueString != null) + { + var bracesStrippedString = defaultValueString.Replace("(", "").Replace(")", "").Trim(); + var bracesAndSingleQuoteStrippedString = bracesStrippedString.Replace("'", ""); if (column.Type == DbType.Int16 || column.Type == DbType.Int32 || column.Type == DbType.Int64) { - column.DefaultValue = long.Parse(column.DefaultValue.ToString()); + column.DefaultValue = long.Parse(bracesStrippedString, CultureInfo.InvariantCulture); } else if (column.Type == DbType.UInt16 || column.Type == DbType.UInt32 || column.Type == DbType.UInt64) { - column.DefaultValue = ulong.Parse(column.DefaultValue.ToString()); + column.DefaultValue = ulong.Parse(bracesStrippedString, CultureInfo.InvariantCulture); } else if (column.Type == DbType.Double || column.Type == DbType.Single) { - column.DefaultValue = double.Parse(column.DefaultValue.ToString()); + column.DefaultValue = double.Parse(bracesAndSingleQuoteStrippedString, CultureInfo.InvariantCulture); } else if (column.Type == DbType.Boolean) { - column.DefaultValue = column.DefaultValue.ToString().Trim() == "1" || column.DefaultValue.ToString().Trim().ToUpper() == "TRUE" || column.DefaultValue.ToString().Trim() == "YES"; + var truthy = new string[] { "'TRUE'", "1" }; + var falsy = new string[] { "'FALSE'", "0" }; + + if (truthy.Contains(bracesStrippedString)) + { + column.DefaultValue = true; + } + else if (falsy.Contains(bracesStrippedString)) + { + column.DefaultValue = false; + } + else if (bracesStrippedString == "NULL") + { + column.DefaultValue = null; + } + else + { + throw new NotImplementedException($"Cannot parse the boolean default value '{defaultValueString}' of column '{column.Name}'"); + } } else if (column.Type == DbType.DateTime || column.Type == DbType.DateTime2) { - if (column.DefaultValue is string defValCv && defValCv.StartsWith("CONVERT(")) + // (CONVERT([datetime],'2000-01-02 03:04:05.000',(121))) + // 121 is a pattern: it contains milliseconds + // Search for 121 here: https://learn.microsoft.com/de-de/sql/t-sql/functions/cast-and-convert-transact-sql?view=sql-server-ver17 + var regexDateTimeConvert121 = new Regex(@"(?<=^\(CONVERT\([\[]+datetime[\]]+,')[^']+(?='\s*,\s*\(121\s*\)\)\)$)"); + var match121 = regexDateTimeConvert121.Match(defaultValueString); + + if (match121.Success) { - var dt = defValCv.Substring((defValCv.IndexOf("'") + 1), defValCv.IndexOf("'", defValCv.IndexOf("'") + 1) - defValCv.IndexOf("'") - 1); - var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); - column.DefaultValue = d; + // We convert to UTC since we restrict date time default values to UTC on default value definition. + column.DefaultValue = DateTime.ParseExact(match121.Value, "yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); } - else if (column.DefaultValue is string defVal) + else if (defaultValueString is string defVal) { + // Not tested var dt = defVal; if (defVal.StartsWith("'")) { dt = defVal.Substring(1, defVal.Length - 2); } - var d = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); - column.DefaultValue = d; + // We convert to UTC since we restrict date time default values to UTC on default value definition. + column.DefaultValue = DateTime.ParseExact(dt, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); + } + else + { + throw new NotImplementedException($"Cannot interpret {column.DefaultValue} in column '{column.Name}' unexpected pattern."); } } else if (column.Type == DbType.Guid) { - if (column.DefaultValue is string defVal) + column.DefaultValue = Guid.Parse(bracesAndSingleQuoteStrippedString); + } + else if (column.MigratorDbType == MigratorDbType.Decimal) + { + // We assume ((1.234)) + column.DefaultValue = decimal.Parse(bracesStrippedString, CultureInfo.InvariantCulture); + } + else if (column.MigratorDbType == MigratorDbType.String) + { + column.DefaultValue = bracesAndSingleQuoteStrippedString; + } + else if (column.MigratorDbType == MigratorDbType.Binary) + { + if (bracesStrippedString.StartsWith("0x")) { - var dt = defVal; - if (defVal.StartsWith("'")) - { - dt = defVal.Substring(1, defVal.Length - 2); - } + var hexString = bracesStrippedString.Substring(2); - var d = Guid.Parse(dt); - column.DefaultValue = d; + // Not available in old .NET version: Convert.FromHexString(hexString); + + column.DefaultValue = Enumerable.Range(0, hexString.Length / 2) + .Select(x => Convert.ToByte(hexString.Substring(x * 2, 2), 16)) + .ToArray(); } + else + { + throw new NotImplementedException($"Cannot parse the binary default value of '{column.Name}'. The value is '{defaultValueString}'"); + } + } + else + { + throw new NotImplementedException($"Cannot parse the default value of {column.Name} type '{column.MigratorDbType}'. It is not yet implemented - file an issue."); } } if (!reader.IsDBNull(5)) diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index b06d0a25..94747d5d 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -14,6 +14,7 @@ using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Framework.Loggers; using DotNetProjects.Migrator.Framework.SchemaBuilder; +using DotNetProjects.Migrator.Providers.Impl.SQLite; using DotNetProjects.Migrator.Providers.Models; using System; using System.Collections.Generic; @@ -269,15 +270,27 @@ public virtual string[] GetTables() public virtual void RemoveForeignKey(string table, string name) { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + RemoveConstraint(table, name); } public virtual void RemoveConstraint(string table, string name) { - if (TableExists(table) && ConstraintExists(table, name)) + if (!TableExists(table)) + { + throw new MigrationException($"Table '{name}' does not exist"); + } + + if (!ConstraintExists(table, name)) { - ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", QuoteTableNameIfRequired(table), QuoteConstraintNameIfRequired(name))); + throw new MigrationException($"Constraint '{name}' does not exist"); } + + ExecuteNonQuery(string.Format("ALTER TABLE {0} DROP CONSTRAINT {1}", QuoteTableNameIfRequired(table), QuoteConstraintNameIfRequired(name))); } public virtual void RemoveAllConstraints(string table) @@ -380,6 +393,11 @@ public virtual void AddView(string name, string tableName, params IViewElement[] /// public virtual void AddTable(string name, params IDbField[] columns) { + if (this is not SQLiteTransformationProvider && columns.Any(x => x is CheckConstraint)) + { + throw new MigrationException($"{nameof(CheckConstraint)}s are currently only supported in SQLite."); + } + // Most databases don't have the concept of a storage engine, so default is to not use it. AddTable(name, null, columns); } @@ -407,6 +425,7 @@ public virtual void AddTable(string name, string engine, params IDbField[] field var compoundPrimaryKey = pks.Count > 1; var columnProviders = new List(columns.Count()); + foreach (var column in columns) { // Remove the primary key notation if compound primary key because we'll add it back later @@ -429,15 +448,17 @@ public virtual void AddTable(string name, string engine, params IDbField[] field } var indexes = fields.Where(x => x is Index).Cast().ToArray(); + foreach (var index in indexes) { AddIndex(name, index); } var foreignKeys = fields.Where(x => x is ForeignKeyConstraint).Cast().ToArray(); + foreach (var foreignKey in foreignKeys) { - this.AddForeignKey(name, foreignKey); + AddForeignKey(name, foreignKey); } } @@ -629,7 +650,7 @@ public virtual void AddColumn(string table, string column, MigratorDbType type, /// AddColumn(string, string, Type, int, ColumnProperty, object) /// /// - public void AddColumn(string table, string column, DbType type) + public virtual void AddColumn(string table, string column, DbType type) { AddColumn(table, column, type, 0, ColumnProperty.Null, null); } @@ -649,7 +670,7 @@ public virtual void AddColumn(string table, string column, MigratorDbType type) /// AddColumn(string, string, Type, int, ColumnProperty, object) /// /// - public void AddColumn(string table, string column, DbType type, int size) + public virtual void AddColumn(string table, string column, DbType type, int size) { AddColumn(table, column, type, size, ColumnProperty.Null, null); }