From b9f1fba84603bcb8c387b975e3525adce1636535 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 21 Aug 2025 18:11:34 +0200 Subject: [PATCH 01/55] Working on AddIndex Fix --- src/Migrator/Framework/Index.cs | 10 ++++- .../Providers/TransformationProvider.cs | 39 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index 92c2e1a4..fc6a2f66 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -3,10 +3,16 @@ public class Index : IDbField { public string Name { get; set; } + public bool Unique { get; set; } + public bool Clustered { get; set; } + public bool PrimaryKey { get; set; } + public bool UniqueConstraint { get; set; } - public string[] KeyColumns { get; set; } - public string[] IncludeColumns { get; set; } + + public string[] KeyColumns { get; set; } = []; + + public string[] IncludeColumns { get; set; } = []; } diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index a9945917..2ef4a48f 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -2089,7 +2089,44 @@ public virtual void RemoveIndex(string table, string name) public virtual void AddIndex(string table, Index index) { - AddIndex(index.Name, table, index.KeyColumns); + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + foreach (var column in index.KeyColumns) + { + if (!ColumnExists(table, column)) + { + throw new MigrationException($"Column '{column}' does not exist."); + } + } + + if (IndexExists(table, index.Name)) + { + throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); + } + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var columnsString = $"({string.Join(", ", columns)})"; + + List list = []; + list.Add("CREATE"); + list.Add(uniqueString); + list.Add("INDEX"); + list.Add(name); + list.Add("ON"); + list.Add(table); + list.Add(columnsString); + + var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + + ExecuteNonQuery(sql); } public virtual void AddIndex(string name, string table, params string[] columns) From 9c048e0ea882d2ce8d4f2823f311124c39478de6 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 21 Aug 2025 18:12:14 +0200 Subject: [PATCH 02/55] Added AddIndex UNIQUE test --- .../Generic/GenericAddIndexTestsBase.cs | 91 +++++++++++++++++++ ...cleTransformationProvider_AddIndexTests.cs | 19 ++++ 2 files changed, 110 insertions(+) create mode 100644 src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs create mode 100644 src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs diff --git a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs new file mode 100644 index 00000000..eda35907 --- /dev/null +++ b/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Framework.SchemaBuilder; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; +using Oracle.ManagedDataAccess.Client; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace Migrator.Tests.Providers.Generic; + +public abstract class GenericAddIndexTestsBase : TransformationProviderBase +{ + [Test] + public void AddIndex_TableDoesNotExist() + { + // Act + Assert.Throws(() => Provider.AddIndex("NotExistingTable", new Index())); + Assert.Throws(() => Provider.AddIndex("NotExistingIndex", "NotExistingTable", "column")); + } + + [Test] + public void AddIndex_UsingIndexInstanceOverload_ShouldBeReadable() + { + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); + + // Arrange + Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName] }); + + // Act + var indexes = Provider.GetIndexes(tableName); + + var index = indexes.Single(); + + Assert.That(index.Name, Is.EqualTo(indexName).IgnoreCase); + Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); + } + + [Test] + public void AddIndex_Unique_ShouldThrowOnSecondInsert() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); + + // Act + Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName], Unique = true }); + + // Assert + Provider.Insert(tableName, [columnName], [1]); + var oracleException = Assert.Throws(() => Provider.Insert(tableName, [columnName], [1])); + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(oracleException.Number, Is.EqualTo(1)); + } + + [Test] + public void AddIndex_UsingNonIndexInstanceOverload_ShouldBeReadable() + { + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); + + // Arrange + Provider.AddIndex(tableName, indexName, columnName); + + // Act + var indexes = Provider.GetIndexes(tableName); + + var index = indexes.Single(); + + Assert.That(index.Name, Is.EqualTo(indexName).IgnoreCase); + Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); + } + + [Test] + public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() + { + + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs new file mode 100644 index 00000000..23c33706 --- /dev/null +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -0,0 +1,19 @@ +using System.Data; +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_AddIndex_Tests : GenericAddIndexTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginOracleTransactionAsync(); + } +} \ No newline at end of file From 1cfc9bca6dc395ddda539ec23bd54e5a44a6dfce Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:35:26 +0200 Subject: [PATCH 03/55] SQLite does not support named primary keys - we check if there is a primary key (there is only one) --- .../Providers/Impl/SQLite/SQLiteTransformationProvider.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index fdb3a0be..bcdbaafc 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -654,7 +654,13 @@ public override void AddPrimaryKey(string name, string tableName, params string[ 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)}."); + var sqliteTableInfo = GetSQLiteTableInfo(table); + + // SQLite does not offer named primary keys BUT since there can only be one primary key we return true if there is any PK. + + var hasPrimaryKey = sqliteTableInfo.Columns.Any(x => x.ColumnProperty.IsSet(ColumnProperty.PrimaryKey)); + + return hasPrimaryKey; } public override void AddUniqueConstraint(string name, string table, params string[] columns) From 4d561f8d6d1fe1efe0c7870bd3a5215a61d9877b Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:35:57 +0200 Subject: [PATCH 04/55] Add comparison for filter items --- src/Migrator/Providers/Dialect.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 557426e7..71d671a5 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -3,6 +3,7 @@ using System.Data; using System.Globalization; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; namespace DotNetProjects.Migrator.Providers; @@ -406,6 +407,19 @@ public ColumnPropertiesMapper GetAndMapColumnPropertiesWithoutDefault(Column col return mapper; } + public string GetComparisonStringFilterIndex(FilterType filterType) + { + return filterType switch + { + FilterType.EqualTo => "=", + FilterType.GreaterThan => ">", + FilterType.GreaterThanOrEqualTo => ">=", + FilterType.SmallerThan => "<", + FilterType.SmallerThanOrEqualTo => "<=", + _ => throw new NotImplementedException("Filter is not implemented yet."), + }; + } + /// /// Subclasses register which DbTypes are unsigned-compatible (ie, available in signed and unsigned variants) /// From f92254819f8f779a4550f437345076df0cd0e902 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:37:42 +0200 Subject: [PATCH 05/55] Add interval for Oracle --- src/Migrator/Framework/Index.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index fc6a2f66..f310da70 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -1,4 +1,6 @@ -namespace DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; + +namespace DotNetProjects.Migrator.Framework; public class Index : IDbField { @@ -15,4 +17,9 @@ public class Index : IDbField public string[] KeyColumns { get; set; } = []; public string[] IncludeColumns { get; set; } = []; + + /// + /// Gets or sets items that represent filter expressions in filtered indexes. Currently string, integer and boolean values are supported. + /// + public FilterItem[] FilterItems { get; set; } = []; } From f6d9cccc77775494c14054a3792bb32b0fc356f3 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:38:17 +0200 Subject: [PATCH 06/55] Add filter items implementation in Transformation Provider --- .../Providers/TransformationProvider.cs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 2ef4a48f..0cc495ba 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -16,6 +16,8 @@ using DotNetProjects.Migrator.Framework.SchemaBuilder; using DotNetProjects.Migrator.Providers.Impl.SQLite; using DotNetProjects.Migrator.Providers.Models; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using System; using System.Collections.Generic; using System.Data; @@ -2114,6 +2116,47 @@ public virtual void AddIndex(string table, Index index) var uniqueString = index.Unique ? "UNIQUE" : null; var columnsString = $"({string.Join(", ", columns)})"; + var filterString = string.Empty; + + if (index.FilterItems != null && index.FilterItems.Length > 0) + { + List singleFilterStrings = []; + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringFilterIndex(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + if (filterItem.Value is bool booleanValue) + { + value = booleanValue ? "1" : "0"; + } + else if (filterItem.Value is string stringValue) + { + value = $"'{stringValue}'"; + } + else if (filterItem.Value is byte || filterItem.Value is short || filterItem.Value is int || filterItem.Value is long) + { + value = Convert.ToInt64(filterItem.Value).ToString(); + } + else if (filterItem.Value is sbyte || filterItem.Value is ushort || filterItem.Value is uint || filterItem.Value is ulong) + { + value = Convert.ToUInt64(filterItem.Value).ToString(); + } + else + { + throw new NotImplementedException("Given type is not implemented. Please file an issue."); + } + + var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; + + singleFilterStrings.Add(singleFilterString); + } + + filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; + } List list = []; list.Add("CREATE"); @@ -2123,6 +2166,7 @@ public virtual void AddIndex(string table, Index index) list.Add("ON"); list.Add(table); list.Add(columnsString); + list.Add(filterString); var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); @@ -2131,13 +2175,9 @@ public virtual void AddIndex(string table, Index index) public virtual void AddIndex(string name, string table, params string[] columns) { - name = QuoteConstraintNameIfRequired(name); - - table = QuoteTableNameIfRequired(table); - - columns = QuoteColumnNamesIfRequired(columns); + var index = new Index { Name = name, KeyColumns = columns }; - ExecuteNonQuery(string.Format("CREATE INDEX {0} ON {1} ({2}) ", name, table, string.Join(", ", columns))); + AddIndex(table, index); } protected string QuoteConstraintNameIfRequired(string name) From 75222cd8a7b6cc193ad7004881bf61f94fb81588 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:38:29 +0200 Subject: [PATCH 07/55] Interval Oracle --- .../Providers/Impl/Oracle/OracleTransformationProvider.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index 79ced05c..a98391e2 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -553,9 +553,13 @@ public override Column[] GetColumns(string table) { column.MigratorDbType = MigratorDbType.String; } + else if (dataTypeString.StartsWith("INTERVAL")) + { + column.MigratorDbType = MigratorDbType.Interval; + } else { - throw new NotImplementedException(); + throw new NotImplementedException($"The data type '{dataTypeString}' is not implemented yet. Please file an issue."); } if (!string.IsNullOrWhiteSpace(dataDefaultString)) From 545e0242526ca5d929b9b13f83f29137a715337e Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:39:07 +0200 Subject: [PATCH 08/55] Rewrite AddIndex in SqlServer --- .../SqlServerTransformationProvider.cs | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index ab41111c..ba318ef7 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -137,23 +137,55 @@ public override void AddPrimaryKeyNonClustered(string name, string table, params string.Join(",", QuoteColumnNamesIfRequired(columns)))); } - public override void AddIndex(string table, Index index) + public override void AddIndex(string name, string table, params string[] columns) { - var name = QuoteConstraintNameIfRequired(index.Name); - - table = QuoteTableNameIfRequired(table); + var index = new Index { Name = name, KeyColumns = columns }; + AddIndex(table, index); + } - var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + public override void AddIndex(string table, Index index) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } - if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) + foreach (var column in index.KeyColumns) { - var include = QuoteColumnNamesIfRequired(index.IncludeColumns); - ExecuteNonQuery(string.Format("CREATE {0}{1} INDEX {2} ON {3} ({4}) INCLUDE ({5})", (index.Unique ? "UNIQUE " : ""), (index.Clustered ? "CLUSTERED" : "NONCLUSTERED"), name, table, string.Join(", ", columns), string.Join(", ", include))); + if (!ColumnExists(table, column)) + { + throw new MigrationException($"Column '{column}' does not exist."); + } } - else + + if (IndexExists(table, index.Name)) { - ExecuteNonQuery(string.Format("CREATE {0}{1} INDEX {2} ON {3} ({4})", (index.Unique ? "UNIQUE " : ""), (index.Clustered ? "CLUSTERED" : "NONCLUSTERED"), name, table, string.Join(", ", columns))); + throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); } + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var clusteredString = index.Clustered ? "CLUSTERED" : "NONCLUSTERED"; + var columnsString = $"({string.Join(", ", columns)})"; + var includedColumnsString = hasIncludedColumns ? $"INCLUDE ({string.Join(", ", QuoteColumnNamesIfRequired(index.IncludeColumns))})" : null; + + List list = []; + list.Add("CREATE"); + list.Add(uniqueString); + list.Add(clusteredString); + list.Add("INDEX"); + list.Add(name); + list.Add(table); + list.Add(columnsString); + list.Add(includedColumnsString); + + var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + + ExecuteNonQuery(sql); } public override void ChangeColumn(string table, Column column) From ed8112d11c4082f85dd5e21a35e2038c94ba489b Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:39:17 +0200 Subject: [PATCH 09/55] Added FilterItem --- .../Providers/Models/Indexes/FilterItem.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Migrator/Providers/Models/Indexes/FilterItem.cs diff --git a/src/Migrator/Providers/Models/Indexes/FilterItem.cs b/src/Migrator/Providers/Models/Indexes/FilterItem.cs new file mode 100644 index 00000000..6d034146 --- /dev/null +++ b/src/Migrator/Providers/Models/Indexes/FilterItem.cs @@ -0,0 +1,21 @@ +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; + +namespace DotNetProjects.Migrator.Providers.Models.Indexes; + +public class FilterItem +{ + /// + /// Gets or sets the not quoted column name. If the column name is not a reserved word it will be converted to lower cased string in Postgre and to upper cased string in Oracle if you use the default settings. + /// + public string ColumnName { get; set; } + + /// + /// Gets or sets the filter. + /// + public FilterType Filter { get; set; } + + /// + /// Gets or sets the value used in the comparison. It needs to be a static not dynamic value. Currently we support bool, byte, short, int, long + /// + public object Value { get; set; } +} \ No newline at end of file From f9ff212715e7d05e3bf511f4eb8fed5bb469b139 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:39:25 +0200 Subject: [PATCH 10/55] Added FilterType --- .../Models/Indexes/Enums/FilterType.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs diff --git a/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs b/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs new file mode 100644 index 00000000..cc5706ac --- /dev/null +++ b/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs @@ -0,0 +1,31 @@ +namespace DotNetProjects.Migrator.Providers.Models.Indexes.Enums; + +public enum FilterType +{ + None = 0, + + /// + /// Greater than + /// + GreaterThan, + + /// + /// Greater than or equal to + /// + GreaterThanOrEqualTo, + + /// + /// Equal to + /// + EqualTo, + + /// + /// Smaller than + /// + SmallerThan, + + /// + /// Smaller than or equal to + /// + SmallerThanOrEqualTo +} \ No newline at end of file From f131a67ec48402f60fa14fee1663881f6e2fbedb Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:39:44 +0200 Subject: [PATCH 11/55] Minor changes in integration tests --- .../Generic/GenericAddIndexTestsBase.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs index eda35907..19ad7e67 100644 --- a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs @@ -1,7 +1,8 @@ -using System; +using System.Data; using System.Linq; using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Framework.SchemaBuilder; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Base; using NUnit.Framework; using Oracle.ManagedDataAccess.Client; @@ -22,16 +23,17 @@ public void AddIndex_TableDoesNotExist() [Test] public void AddIndex_UsingIndexInstanceOverload_ShouldBeReadable() { + // Arrange const string tableName = "TestTable"; const string columnName = "TestColumn"; const string indexName = "TestIndexName"; Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); - // Arrange + // Act Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName] }); - // Act + // Assert var indexes = Provider.GetIndexes(tableName); var index = indexes.Single(); @@ -65,16 +67,17 @@ public void AddIndex_Unique_ShouldThrowOnSecondInsert() [Test] public void AddIndex_UsingNonIndexInstanceOverload_ShouldBeReadable() { + // Arrange const string tableName = "TestTable"; const string columnName = "TestColumn"; const string indexName = "TestIndexName"; Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); - // Arrange - Provider.AddIndex(tableName, indexName, columnName); - // Act + Provider.AddIndex(indexName, tableName, columnName); + + // Assert var indexes = Provider.GetIndexes(tableName); var index = indexes.Single(); @@ -83,9 +86,5 @@ public void AddIndex_UsingNonIndexInstanceOverload_ShouldBeReadable() Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); } - [Test] - public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() - { - } } \ No newline at end of file From d802a515214d5e5ba6834e369660db0ebce131e0 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 11:45:02 +0200 Subject: [PATCH 12/55] Small refactoring --- src/Migrator/Providers/Dialect.cs | 54 +++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 71d671a5..34dd066e 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -12,10 +12,10 @@ namespace DotNetProjects.Migrator.Providers; /// public abstract class Dialect : IDialect { - private readonly Dictionary propertyMap = []; - private readonly HashSet reservedWords = []; - private readonly TypeNames typeNames = new(); - private readonly List unsignedCompatibleTypes = []; + private readonly Dictionary _propertyMap = []; + private readonly HashSet _reservedWords = []; + private readonly TypeNames _typeNames = new(); + private readonly List _unsignedCompatibleTypes = []; protected Dialect() { @@ -82,7 +82,7 @@ public virtual bool NeedsNullForNullableWhenAlteringTable protected void AddReservedWord(string reservedWord) { - reservedWords.Add(reservedWord.ToUpperInvariant()); + _reservedWords.Add(reservedWord.ToUpperInvariant()); } protected void AddReservedWords(params string[] words) @@ -94,7 +94,7 @@ protected void AddReservedWords(params string[] words) foreach (var word in words) { - reservedWords.Add(word); + _reservedWords.Add(word); } } @@ -105,12 +105,12 @@ public virtual bool IsReservedWord(string reservedWord) throw new ArgumentNullException("reservedWord"); } - if (reservedWords == null) + if (_reservedWords == null) { return false; } - var isReserved = reservedWords.Contains(reservedWord.ToUpperInvariant()); + var isReserved = _reservedWords.Contains(reservedWord.ToUpperInvariant()); if (isReserved) { @@ -143,7 +143,7 @@ public ITransformationProvider NewProviderForDialect(IDbConnection connection, s /// The database type name protected void RegisterColumnType(DbType code, int capacity, string name) { - typeNames.Put(code, capacity, name); + _typeNames.Put(code, capacity, name); } /// @@ -156,7 +156,7 @@ protected void RegisterColumnType(DbType code, int capacity, string name) /// The database type name protected void RegisterColumnType(MigratorDbType code, int capacity, string name) { - typeNames.Put(code, capacity, name); + _typeNames.Put(code, capacity, name); } /// @@ -171,7 +171,7 @@ protected void RegisterColumnType(MigratorDbType code, int capacity, string name /// The database type name protected void RegisterColumnTypeWithPrecision(DbType code, string name) { - typeNames.Put(code, -1, name); + _typeNames.Put(code, -1, name); } /// @@ -182,7 +182,7 @@ protected void RegisterColumnTypeWithPrecision(DbType code, string name) /// The database type name protected void RegisterColumnType(MigratorDbType code, string name) { - typeNames.Put(code, name); + _typeNames.Put(code, name); } /// @@ -193,7 +193,7 @@ protected void RegisterColumnType(MigratorDbType code, string name) /// The database type name protected void RegisterColumnType(DbType code, string name) { - typeNames.Put(code, name); + _typeNames.Put(code, name); } /// @@ -205,13 +205,13 @@ protected void RegisterColumnType(DbType code, string name) /// The database type name protected void RegisterColumnTypeWithParameters(DbType code, string name) { - typeNames.PutParametrized(code, name); + _typeNames.PutParametrized(code, name); } protected void RegisterColumnTypeAlias(DbType code, string alias) { - typeNames.PutAlias(code, alias); + _typeNames.PutAlias(code, alias); } public virtual ColumnPropertiesMapper GetColumnMapper(Column column) @@ -233,7 +233,7 @@ public virtual ColumnPropertiesMapper GetColumnMapper(Column column) public virtual DbType GetDbTypeFromString(string type) { - return typeNames.GetDbType(type); + return _typeNames.GetDbType(type); } /// @@ -243,7 +243,7 @@ public virtual DbType GetDbTypeFromString(string type) /// The database type name used by ddl. public virtual string GetTypeName(DbType type) { - var result = typeNames.Get(type); + var result = _typeNames.Get(type); if (result == null) { @@ -274,7 +274,7 @@ public virtual string GetTypeName(DbType type, int length) /// public virtual string GetTypeName(DbType type, int length, int precision, int scale) { - var resultWithLength = typeNames.Get(type, length, precision, scale); + var resultWithLength = _typeNames.Get(type, length, precision, scale); if (resultWithLength != null) { return resultWithLength; @@ -293,7 +293,7 @@ public virtual string GetTypeName(DbType type, int length, int precision, int sc /// public virtual string GetTypeNameParametrized(DbType type, int length, int precision, int scale) { - var result = typeNames.GetParametrized(type); + var result = _typeNames.GetParametrized(type); if (result != null) { return result.Replace("{length}", length.ToString()) @@ -312,23 +312,23 @@ public virtual string GetTypeNameParametrized(DbType type, int length, int preci /// The . public virtual DbType GetDbType(string databaseTypeName) { - return typeNames.GetDbType(databaseTypeName); + return _typeNames.GetDbType(databaseTypeName); } public void RegisterProperty(ColumnProperty property, string sql) { - if (!propertyMap.ContainsKey(property)) + if (!_propertyMap.ContainsKey(property)) { - propertyMap.Add(property, sql); + _propertyMap.Add(property, sql); } - propertyMap[property] = sql; + _propertyMap[property] = sql; } public virtual string SqlForProperty(ColumnProperty property, Column column) { - if (propertyMap.ContainsKey(property)) + if (_propertyMap.ContainsKey(property)) { - return propertyMap[property]; + return _propertyMap[property]; } return string.Empty; } @@ -426,7 +426,7 @@ public string GetComparisonStringFilterIndex(FilterType filterType) /// protected void RegisterUnsignedCompatible(DbType type) { - unsignedCompatibleTypes.Add(type); + _unsignedCompatibleTypes.Add(type); } /// @@ -436,7 +436,7 @@ protected void RegisterUnsignedCompatible(DbType type) /// True if the database type has an unsigned variant, otherwise false public bool IsUnsignedCompatible(DbType type) { - return unsignedCompatibleTypes.Contains(type); + return _unsignedCompatibleTypes.Contains(type); } } From 33e94cee6acc494e00c0445934297d63daf7fa9a Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 13:11:07 +0200 Subject: [PATCH 13/55] Added SQL Server implementation of filtered index --- ...verTransformationProvider_AddIndexTests.cs | 57 +++++++++++++++++++ .../SqlServerTransformationProvider.cs | 51 +++++++++++++++-- .../Providers/TransformationProvider.cs | 2 +- 3 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs new file mode 100644 index 00000000..6615c538 --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -0,0 +1,57 @@ +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLServer; + +[TestFixture] +[Category("SqlServer")] +public class SQLServerTransformationProvider_AddIndexTests : TransformationProviderBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLServerTransactionAsync(); + } + + [Test] + public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName, columnName2], + Unique = true, + FilterItems = [ + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName, Value = 100 }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName2, Value = "Hello" }, + ] + }); + + // Assert + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + // Unique but no exception is thrown since smaller than 100 + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + + Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"]); + var sqlException = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"])); + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(sqlException.Number, Is.EqualTo(2601)); + } +} diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index ba318ef7..60954e81 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -169,9 +169,49 @@ public override void AddIndex(string table, Index index) var columns = QuoteColumnNamesIfRequired(index.KeyColumns); var uniqueString = index.Unique ? "UNIQUE" : null; - var clusteredString = index.Clustered ? "CLUSTERED" : "NONCLUSTERED"; var columnsString = $"({string.Join(", ", columns)})"; - var includedColumnsString = hasIncludedColumns ? $"INCLUDE ({string.Join(", ", QuoteColumnNamesIfRequired(index.IncludeColumns))})" : null; + var filterString = string.Empty; + var clusteredString = index.Clustered ? "CLUSTERED" : "NONCLUSTERED"; + + if (index.FilterItems != null && index.FilterItems.Length > 0) + { + List singleFilterStrings = []; + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringFilterIndex(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + if (filterItem.Value is bool booleanValue) + { + value = booleanValue ? "1" : "0"; + } + else if (filterItem.Value is string stringValue) + { + value = $"'{stringValue}'"; + } + else if (filterItem.Value is byte || filterItem.Value is short || filterItem.Value is int || filterItem.Value is long) + { + value = Convert.ToInt64(filterItem.Value).ToString(); + } + else if (filterItem.Value is sbyte || filterItem.Value is ushort || filterItem.Value is uint || filterItem.Value is ulong) + { + value = Convert.ToUInt64(filterItem.Value).ToString(); + } + else + { + throw new NotImplementedException("Given type is not implemented. Please file an issue."); + } + + var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; + + singleFilterStrings.Add(singleFilterString); + } + + filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; + } List list = []; list.Add("CREATE"); @@ -179,11 +219,14 @@ public override void AddIndex(string table, Index index) list.Add(clusteredString); list.Add("INDEX"); list.Add(name); + list.Add("ON"); list.Add(table); list.Add(columnsString); - list.Add(includedColumnsString); + list.Add(filterString); + + list = [.. list.Where(x => !string.IsNullOrWhiteSpace(x))]; - var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + var sql = string.Join(" ", list); ExecuteNonQuery(sql); } diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 0cc495ba..b7a415dd 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -1351,7 +1351,7 @@ public virtual int Insert(string table, string[] columns, object[] values) if (columns.Length != values.Length) { - throw new Exception(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); + throw new MigrationException(string.Format("The number of columns: {0} does not match the number of supplied values: {1}", columns.Length, values.Length)); } table = QuoteTableNameIfRequired(table); From 53665ebc1461054ebffe5f4ea979ebb34ccd2e5e Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 13:45:00 +0200 Subject: [PATCH 14/55] Added Postgre AddIndex UNIQUE test --- .../Generic/GenericAddIndexTestsBase.cs | 5 -- ...cleTransformationProvider_AddIndexTests.cs | 6 +- ...SQLTransformationProvider_AddIndexTests.cs | 64 +++++++++++++++++++ .../Providers/TransformationProvider.cs | 5 +- 4 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs diff --git a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs index 19ad7e67..d25ec4fd 100644 --- a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs @@ -1,8 +1,5 @@ -using System.Data; using System.Linq; using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Providers.Models.Indexes; -using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Base; using NUnit.Framework; using Oracle.ManagedDataAccess.Client; @@ -85,6 +82,4 @@ public void AddIndex_UsingNonIndexInstanceOverload_ShouldBeReadable() Assert.That(index.Name, Is.EqualTo(indexName).IgnoreCase); Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); } - - } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index 23c33706..1e9ffcfc 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -1,9 +1,11 @@ using System.Data; +using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; -using Migrator.Tests.Providers.Base; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; +using Oracle.ManagedDataAccess.Client; namespace Migrator.Tests.Providers.OracleProvider; @@ -16,4 +18,6 @@ public async Task SetUpAsync() { await BeginOracleTransactionAsync(); } + + } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs new file mode 100644 index 00000000..c82f23dd --- /dev/null +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -0,0 +1,64 @@ +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Generic; +using Npgsql; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.PostgreSQL; + +[TestFixture] +[Category("Postgre")] +public class PostgreSQLTransformationProvider_AddIndexTests : GenericAddIndexTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginPostgreSQLTransactionAsync(); + } + + [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"); + } + + [Test] + public void AddIndex_Unique_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + }); + + // Assert + var indexes = Provider.GetIndexes(tableName); + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [1, "Some other string"])); + var index = indexes.Single(); + + Assert.That(index.Unique, Is.True); + // Need to compare message string since ErrorNumber does not hold a positive number. + Assert.That(ex.Message, Does.StartWith("23505: duplicate key value violates unique constraint")); + Assert.That(ex.SqlState, Is.EqualTo("23505")); + } +} diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index b7a415dd..7027fc25 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -886,7 +886,7 @@ public virtual int ExecuteNonQuery(string sql) public virtual int ExecuteNonQuery(string sql, int timeout) { - return this.ExecuteNonQuery(sql, timeout, null); + return ExecuteNonQuery(sql, timeout, null); } public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args) @@ -903,6 +903,7 @@ public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args } using var cmd = BuildCommand(sql); + try { cmd.CommandTimeout = timeout; @@ -927,7 +928,7 @@ public virtual int ExecuteNonQuery(string sql, int timeout, params object[] args catch (Exception ex) { Logger.Warn(ex.Message); - throw new Exception(string.Format("Error occured executing sql: {0}, see inner exception for details, error: " + ex, sql), ex); + throw new MigrationException(string.Format("Error occured executing sql: {0}, see inner exception for details, error: " + ex, sql), ex); } } From db4b0ad1e21e461485526379af8bc1043bd5f9bd Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 13:45:23 +0200 Subject: [PATCH 15/55] Added SQL AddIndex UNIQUE test --- ...verTransformationProvider_AddIndexTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index 6615c538..2a50eb3c 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -18,6 +18,35 @@ public async Task SetUpAsync() await BeginSQLServerTransactionAsync(); } + [Test] + public void AddIndex_Unique_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + }); + + // Assert + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + var sqlException = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [1, "Some other string"])); + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(sqlException.Number, Is.EqualTo(2601)); + } + [Test] public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() { From 30cb42cae0a0857aa1e581cdcead1df86c93551f Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 13:53:56 +0200 Subject: [PATCH 16/55] Oracle unique test --- ...cleTransformationProvider_AddIndexTests.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index 1e9ffcfc..c56b7541 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; using Oracle.ManagedDataAccess.Client; @@ -19,5 +18,32 @@ public async Task SetUpAsync() await BeginOracleTransactionAsync(); } + [Test] + public void AddIndex_Unique_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + }); + // Assert + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [1, "Some other string"])); + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(ex.Number, Is.EqualTo(1)); + } } \ No newline at end of file From d25394e82808659fcd072c8b338f3059e1894670 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 14:30:54 +0200 Subject: [PATCH 17/55] SQLite Unique test --- ...iteTransformationProvider_AddIndexTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs new file mode 100644 index 00000000..0ad48af9 --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -0,0 +1,49 @@ +using System.Data; +using System.Data.SQLite; +using System.Linq; +using System.Threading.Tasks; +using DotNetProjects.Migrator.Framework; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLite; + +[TestFixture] +[Category("SQLite")] +public class SQLiteTransformationProvider_AddIndexTests : Generic_AddIndexTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLiteTransactionAsync(); + } + + [Test] + public void AddIndex_Unique_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + }); + + // Assert + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [1, "Some other string"])); + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(ex.ErrorCode, Is.EqualTo(19)); + } +} From 831c7eed899a8d3c94de2a504cc7d005aaeba4d5 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 14:31:31 +0200 Subject: [PATCH 18/55] SQLite Unique index --- .../Impl/SQLite/SQLiteTransformationProvider.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index bcdbaafc..357be507 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1198,12 +1198,9 @@ public override Index[] GetIndexes(string table) var pragmaIndexListItems = GetPragmaIndexListItems(table); - // Since unique indexes are supported but only by using unique constraints or primary keys we filter them out here. See "GetUniques()" for unique constraints. - var pragmaIndexListItemsFiltered = pragmaIndexListItems.Where(x => !x.Unique).ToList(); - - foreach (var pragmaIndexListItemFiltered in pragmaIndexListItemsFiltered) + foreach (var pragmaIndexListItem in pragmaIndexListItems) { - var indexInfos = GetPragmaIndexInfo(pragmaIndexListItemFiltered.Name); + var indexInfos = GetPragmaIndexInfo(pragmaIndexListItem.Name); var columnNames = indexInfos.OrderBy(x => x.SeqNo) .Select(x => x.Name) @@ -1218,10 +1215,8 @@ public override Index[] GetIndexes(string table) // SQLite does not support include colums IncludeColumns = [], KeyColumns = columnNames, - Name = pragmaIndexListItemFiltered.Name, - - // See GetUniques() - Unique = false, + Name = pragmaIndexListItem.Name, + Unique = pragmaIndexListItem.Origin == "c" }; indexes.Add(index); @@ -1419,8 +1414,7 @@ public List GetUniques(string tableName) // Here we filter for origin u and unique while in "GetIndexes()" we exclude them. // If "pk" is set then it was added by using a primary key. If so this is handled by "GetColumns()". - // If "c" is set it was created by using CREATE INDEX. At this moment in time this migrator does not support UNIQUE indexes but only normal indexes - // so "u" should never be set 30.06.2025). + // If "c" is set it was created by using CREATE INDEX. var uniqueConstraints = pragmaIndexListItems.Where(x => x.Unique && x.Origin == "u") .ToList(); From b8ee6e6c005a2fbb325d0e4bb3eda49bbcbd1be9 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 14:31:54 +0200 Subject: [PATCH 19/55] Renaming --- ...GenericAddIndexTestsBase.cs => Generic_AddIndexTestsBase.cs} | 2 +- .../OracleTransformationProvider_AddIndexTests.cs | 2 +- .../PostgreSQLTransformationProvider_AddIndexTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/Migrator.Tests/Providers/Generic/{GenericAddIndexTestsBase.cs => Generic_AddIndexTestsBase.cs} (97%) diff --git a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs similarity index 97% rename from src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs rename to src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index d25ec4fd..d2a6e204 100644 --- a/src/Migrator.Tests/Providers/Generic/GenericAddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -7,7 +7,7 @@ namespace Migrator.Tests.Providers.Generic; -public abstract class GenericAddIndexTestsBase : TransformationProviderBase +public abstract class Generic_AddIndexTestsBase : TransformationProviderBase { [Test] public void AddIndex_TableDoesNotExist() diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index c56b7541..09d97d41 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -10,7 +10,7 @@ namespace Migrator.Tests.Providers.OracleProvider; [TestFixture] [Category("Oracle")] -public class OracleTransformationProvider_AddIndex_Tests : GenericAddIndexTestsBase +public class OracleTransformationProvider_AddIndex_Tests : Generic_AddIndexTestsBase { [SetUp] public async Task SetUpAsync() diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index c82f23dd..f5f5925b 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -10,7 +10,7 @@ namespace Migrator.Tests.Providers.PostgreSQL; [TestFixture] [Category("Postgre")] -public class PostgreSQLTransformationProvider_AddIndexTests : GenericAddIndexTestsBase +public class PostgreSQLTransformationProvider_AddIndexTests : Generic_AddIndexTestsBase { [SetUp] public async Task SetUpAsync() From df5bbf1ad8a67ca2934e3e7d13f5f81f1aee17a0 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 15:43:34 +0200 Subject: [PATCH 20/55] Changes due to index changes. --- .../Generic/Generic_AddIndexTestsBase.cs | 22 ------------------- ...mationProviderGenericMiscConstraintBase.cs | 9 +------- ...iteTransformationProvider_AddTableTests.cs | 6 ++--- ...SQLiteTransformationProvider_GetUniques.cs | 16 ++++++++++++++ .../SQLite/SQLiteTransformationProvider.cs | 4 ++-- 5 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index d2a6e204..f79f8f78 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -39,28 +39,6 @@ public void AddIndex_UsingIndexInstanceOverload_ShouldBeReadable() Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); } - [Test] - public void AddIndex_Unique_ShouldThrowOnSecondInsert() - { - // Arrange - const string tableName = "TestTable"; - const string columnName = "TestColumn"; - const string indexName = "TestIndexName"; - - Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); - - // Act - Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName], Unique = true }); - - // Assert - Provider.Insert(tableName, [columnName], [1]); - var oracleException = Assert.Throws(() => Provider.Insert(tableName, [columnName], [1])); - var index = Provider.GetIndexes(tableName).Single(); - - Assert.That(index.Unique, Is.True); - Assert.That(oracleException.Number, Is.EqualTo(1)); - } - [Test] public void AddIndex_UsingNonIndexInstanceOverload_ShouldBeReadable() { diff --git a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs index 84ca3c1d..d2d2a180 100644 --- a/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs +++ b/src/Migrator.Tests/Providers/Generic/TransformationProviderGenericMiscConstraintBase.cs @@ -44,14 +44,7 @@ public void CanAddPrimaryKey() { AddPrimaryKey(); - if (Provider is SQLiteTransformationProvider) - { - Assert.Throws(() => Provider.PrimaryKeyExists("Test", "PK_Test")); - } - else - { - Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True); - } + Assert.That(Provider.PrimaryKeyExists("Test", "PK_Test"), Is.True); } // [Test] diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index 50fad73a..8f8fe486 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -44,7 +44,7 @@ public void AddTable_CompositePrimaryKey_ContainsNull() ); Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)"); - Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)")); + Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)")); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); @@ -73,7 +73,7 @@ public void AddTable_SinglePrimaryKey_ContainsNull() ); Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)"); - Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,2)")); + Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,2)")); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); @@ -101,7 +101,7 @@ public void AddTable_MiscellaneousColumns_Succeeds() ); Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)"); - Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)")); + Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)")); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs index 34093be0..d4727474 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetUniques.cs @@ -23,6 +23,8 @@ public void GetUniques_Success() const string property5 = "Property5"; const string uniqueConstraintName1 = "UniqueConstraint1"; const string uniqueConstraintName2 = "UniqueConstraint2"; + const string uniqueIndexName1 = "IndexUnique1"; + const string nonUniqueIndexName1 = "IndexNonUnique1"; Provider.AddTable(tableNameA, new Column(property1, DbType.Int32, ColumnProperty.PrimaryKey), @@ -35,8 +37,13 @@ public void GetUniques_Success() Provider.AddUniqueConstraint(uniqueConstraintName1, tableNameA, property3); Provider.AddUniqueConstraint(uniqueConstraintName2, tableNameA, property4, property5); + // Add unique index in order to assert there is no interference with unique constraints + Provider.AddIndex(tableNameA, new Index { Name = nonUniqueIndexName1, KeyColumns = [property4], Unique = false }); + Provider.AddIndex(tableNameA, new Index { Name = uniqueIndexName1, KeyColumns = [property5], Unique = true }); + // Act var uniqueConstraints = ((SQLiteTransformationProvider)Provider).GetUniques(tableNameA); + var indexes = Provider.GetIndexes(tableNameA); // Assert var sql = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableNameA); @@ -48,5 +55,14 @@ public void GetUniques_Success() Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint1 UNIQUE (Property3)")); Assert.That(sql, Does.Contain("CONSTRAINT UniqueConstraint2 UNIQUE (Property4, Property5)")); Assert.That(sql, Does.Contain("CONSTRAINT sqlite_autoindex_TableA_1 UNIQUE (Property2)")); + + var retrievedUniqueIndex1 = indexes.Single(x => x.Name == uniqueIndexName1); + + Assert.That(retrievedUniqueIndex1.Unique, Is.True); + Assert.That(retrievedUniqueIndex1.Name, Is.EqualTo(uniqueIndexName1)); + + var retrievedNonUniqueIndex1 = indexes.Single(x => x.Name == nonUniqueIndexName1); + + Assert.That(retrievedNonUniqueIndex1.Unique, Is.False); } } diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 357be507..932b69da 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1196,7 +1196,7 @@ public override Index[] GetIndexes(string table) { List indexes = []; - var pragmaIndexListItems = GetPragmaIndexListItems(table); + var pragmaIndexListItems = GetPragmaIndexListItems(table).Where(x => x.Origin == "c"); foreach (var pragmaIndexListItem in pragmaIndexListItems) { @@ -1216,7 +1216,7 @@ public override Index[] GetIndexes(string table) IncludeColumns = [], KeyColumns = columnNames, Name = pragmaIndexListItem.Name, - Unique = pragmaIndexListItem.Origin == "c" + Unique = pragmaIndexListItem.Unique }; indexes.Add(index); From c3eb4cabc6fe8af92248f6af17b998e01aa4ca81 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Fri, 22 Aug 2025 16:09:07 +0200 Subject: [PATCH 21/55] Added SQLite Test AddIndex filtered --- .../Dialects/PostgreDialectTests.cs | 30 +++++++++++++++ .../Generic/Generic_AddIndexTestsBase.cs | 1 - ...verTransformationProvider_AddIndexTests.cs | 4 +- ...iteTransformationProvider_AddIndexTests.cs | 38 +++++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/Migrator.Tests/Dialects/PostgreDialectTests.cs diff --git a/src/Migrator.Tests/Dialects/PostgreDialectTests.cs b/src/Migrator.Tests/Dialects/PostgreDialectTests.cs new file mode 100644 index 00000000..d8535f2d --- /dev/null +++ b/src/Migrator.Tests/Dialects/PostgreDialectTests.cs @@ -0,0 +1,30 @@ +using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; +using NUnit.Framework; + +namespace Migrator.Tests.Dialects; + +[TestFixture] +[Category("Postgre")] +public class PostgreDialectTests +{ + private PostgreSQLDialect _postgreSQLDialect; + + [SetUp] + public void SetUp() + { + _postgreSQLDialect = new PostgreSQLDialect(); + } + + [TestCase(FilterType.EqualTo, "=")] + [TestCase(FilterType.GreaterThanOrEqualTo, ">=")] + [TestCase(FilterType.SmallerThanOrEqualTo, "<=")] + [TestCase(FilterType.SmallerThan, "<")] + [TestCase(FilterType.GreaterThan, ">")] + public void GetComparisonStringFilterIndex(FilterType filterType, string expectedString) + { + var result = _postgreSQLDialect.GetComparisonStringFilterIndex(filterType); + + Assert.That(result, Is.EqualTo(expectedString)); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index f79f8f78..9c5d9e59 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -2,7 +2,6 @@ using DotNetProjects.Migrator.Framework; using Migrator.Tests.Providers.Base; using NUnit.Framework; -using Oracle.ManagedDataAccess.Client; using Index = DotNetProjects.Migrator.Framework.Index; namespace Migrator.Tests.Providers.Generic; diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index 2a50eb3c..73e4cb78 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -3,14 +3,14 @@ using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; -using Migrator.Tests.Providers.Base; +using Migrator.Tests.Providers.Generic; using NUnit.Framework; namespace Migrator.Tests.Providers.SQLServer; [TestFixture] [Category("SqlServer")] -public class SQLServerTransformationProvider_AddIndexTests : TransformationProviderBase +public class SQLServerTransformationProvider_AddIndexTests : Generic_AddIndexTestsBase { [SetUp] public async Task SetUpAsync() diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs index 0ad48af9..14e0ad63 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; @@ -46,4 +47,41 @@ public void AddIndex_Unique_Success() Assert.That(index.Unique, Is.True); Assert.That(ex.ErrorCode, Is.EqualTo(19)); } + + [Test] + public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName, columnName2], + Unique = true, + FilterItems = [ + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName, Value = 100 }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName2, Value = "Hello" }, + ] + }); + + // Assert + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + // Unique but no exception is thrown since smaller than 100 + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + + Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"]); + var sqliteException = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"])); + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(sqliteException.ErrorCode, Is.EqualTo(19)); + } } From 00da295a2e484ba8aaf7e806add98a531a346765 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 08:09:35 +0200 Subject: [PATCH 22/55] AddTableTest changes --- .../Generic/Generic_AddIndexTestsBase.cs | 4 +- ...iteTransformationProvider_AddIndexTests.cs | 59 ++++++++++++++----- ...iteTransformationProvider_AddTableTests.cs | 22 +++---- ...eTransformationProvider_GetColumnsTests.cs | 23 +++++--- ...eTransformationProvider_GetIndexesTests.cs | 16 +++++ 5 files changed, 86 insertions(+), 38 deletions(-) create mode 100644 src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetIndexesTests.cs diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index 9c5d9e59..777b11b1 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -17,7 +17,7 @@ public void AddIndex_TableDoesNotExist() } [Test] - public void AddIndex_UsingIndexInstanceOverload_ShouldBeReadable() + public void AddIndex_UsingIndexInstanceOverload_NonUnique_ShouldBeReadable() { // Arrange const string tableName = "TestTable"; @@ -39,7 +39,7 @@ public void AddIndex_UsingIndexInstanceOverload_ShouldBeReadable() } [Test] - public void AddIndex_UsingNonIndexInstanceOverload_ShouldBeReadable() + public void AddIndex_UsingNonIndexInstanceOverload_NonUnique_ShouldBeReadable() { // Arrange const string tableName = "TestTable"; diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs index 14e0ad63..f81f49df 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -23,28 +23,30 @@ public async Task SetUpAsync() public void AddIndex_Unique_Success() { // Arrange - const string tableName = "TestTable"; - const string columnName = "TestColumn"; + const string columnName1 = "TestColumn"; const string columnName2 = "TestColumn2"; const string indexName = "TestIndexName"; + const string tableName = "TestTable"; - Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + Provider.AddTable(tableName, new Column(columnName1, DbType.Int32), new Column(columnName2, DbType.String)); // Act Provider.AddIndex(tableName, new Index { + KeyColumns = [columnName1], Name = indexName, - KeyColumns = [columnName], Unique = true, }); // Assert - Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); - var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [1, "Some other string"])); + Provider.Insert(tableName, [columnName1, columnName2], [1, "Hello"]); + var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, "Some other string"])); var index = Provider.GetIndexes(tableName).Single(); Assert.That(index.Unique, Is.True); + + // Unique violation Assert.That(ex.ErrorCode, Is.EqualTo(19)); } @@ -52,36 +54,61 @@ public void AddIndex_Unique_Success() public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() { // Arrange - const string tableName = "TestTable"; - const string columnName = "TestColumn"; + const string columnName1 = "TestColumn"; const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string columnName4 = "TestColumn4"; const string indexName = "TestIndexName"; + const string tableName = "TestTable"; - Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int32), + new Column(columnName2, DbType.String), + new Column(columnName3, DbType.Boolean), + new Column(columnName4, DbType.Int32) + ); // Act Provider.AddIndex(tableName, new Index { Name = indexName, - KeyColumns = [columnName, columnName2], + KeyColumns = [columnName1, columnName2, columnName3], Unique = true, FilterItems = [ - new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName, Value = 100 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName1, Value = 100 }, new() { Filter = FilterType.EqualTo, ColumnName = columnName2, Value = "Hello" }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName3, Value = true }, ] }); + // We remove column to invoke a recreation of the table. + Provider.RemoveColumn(tableName, columnName4); + // Assert - Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); - // Unique but no exception is thrown since smaller than 100 - Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + Provider.Insert(tableName, [columnName1, columnName2, columnName3], [1, "Hello", true]); + // Unique but no exception should be thrown since the integer value is smaller than 100 - not within the filter restriction. + Provider.Insert(tableName, [columnName1, columnName2, columnName3], [1, "Hello", true]); - Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"]); - var sqliteException = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"])); + Provider.Insert(tableName, [columnName1, columnName2, columnName3], [100, "Hello", true]); + var sqliteException = Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2, columnName3], [100, "Hello", true])); var index = Provider.GetIndexes(tableName).Single(); Assert.That(index.Unique, Is.True); + // Unique violation Assert.That(sqliteException.ErrorCode, Is.EqualTo(19)); + + var indexScriptFromDatabase = GetCreateIndexSqlString(indexName); + + Assert.That(indexScriptFromDatabase, Is.EqualTo("CREATE UNIQUE INDEX TestIndexName ON TestTable (TestColumn, TestColumn2, TestColumn3) WHERE TestColumn >= 100 AND TestColumn2 = 'Hello' AND TestColumn3 = 1")); + } + + private string GetCreateIndexSqlString(string indexName) + { + using var cmd = Provider.CreateCommand(); + using var reader = Provider.ExecuteQuery(cmd, $"SELECT sql FROM sqlite_master WHERE type='index' AND lower(name)=lower('{indexName}')"); + reader.Read(); + + return reader.IsDBNull(0) ? null : (string)reader[0]; } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index 8f8fe486..3cea2832 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -12,7 +12,7 @@ namespace Migrator.Tests.Providers.SQLite; public class SQLiteTransformationProvider_AddTableTests : SQLiteTransformationProviderTestBase { [Test] - public void AddTable_UniqueOnly_ContainsNull() + public void AddTable_UniqueOnlyOnColumnLevel_Obsolete_UniquesListIsEmpty() { const string tableName = "MyTableName"; const string columnName = "MyColumnName"; @@ -22,7 +22,7 @@ public void AddTable_UniqueOnly_ContainsNull() // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That("CREATE TABLE MyTableName (MyColumnName INTEGER NULL UNIQUE)", Is.EqualTo(createScript)); + Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (MyColumnName INTEGER NULL UNIQUE)")); var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); @@ -43,12 +43,12 @@ public void AddTable_CompositePrimaryKey_ContainsNull() new Column(columnName2, System.Data.DbType.Int32, ColumnProperty.PrimaryKey | ColumnProperty.NotNull) ); - Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)"); - Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)")); + Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That("CREATE TABLE MyTableName (Column1 INTEGER NULL, Column2 INTEGER NOT NULL, PRIMARY KEY (Column1, Column2))", Is.EqualTo(createScript)); + Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NULL, Column2 INTEGER NOT NULL, PRIMARY KEY (Column1, Column2))")); var pragmaTableInfos = ((SQLiteTransformationProvider)Provider).GetPragmaTableInfoItems(tableName); Assert.That(pragmaTableInfos.Single(x => x.Name == columnName1).NotNull, Is.False); @@ -72,12 +72,12 @@ public void AddTable_SinglePrimaryKey_ContainsNull() new Column(columnName2, System.Data.DbType.Int32, ColumnProperty.NotNull) ); - Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)"); - Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,2)")); + Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 2])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY, Column2 INTEGER NOT NULL)", Is.EqualTo(createScript)); + Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY, Column2 INTEGER NOT NULL)")); var pragmaTableInfos = ((SQLiteTransformationProvider)Provider).GetPragmaTableInfoItems(tableName); Assert.That(pragmaTableInfos.All(x => x.NotNull), Is.True); @@ -100,12 +100,12 @@ public void AddTable_MiscellaneousColumns_Succeeds() new Column(columnName2, System.Data.DbType.Int32, ColumnProperty.Null | ColumnProperty.Unique) ); - Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)"); - Assert.Throws(() => Provider.ExecuteNonQuery($"INSERT INTO {tableName} ({columnName1}, {columnName2}) VALUES (1,1)")); + Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); - Assert.That("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, Column2 INTEGER NULL UNIQUE)", Is.EqualTo(createScript)); + Assert.That(createScript, Is.EqualTo("CREATE TABLE MyTableName (Column1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, Column2 INTEGER NULL UNIQUE)")); var pragmaTableInfos = ((SQLiteTransformationProvider)Provider).GetPragmaTableInfoItems(tableName); Assert.That(pragmaTableInfos.First().NotNull, Is.True); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs index 3e7a048e..f8e720ae 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetColumnsTests.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; @@ -22,7 +23,7 @@ public void GetColumns_UniqueButNotPrimaryKey_ReturnsFalse() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Id", System.Data.DbType.Int32, ColumnProperty.Unique)); + Provider.AddTable(tableName, new Column("Id", DbType.Int32, ColumnProperty.Unique)); // Act var columns = Provider.GetColumns(tableName); @@ -36,7 +37,7 @@ public void GetColumns_PrimaryAndUnique_ReturnsFalse() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Id", System.Data.DbType.Int32, ColumnProperty.Unique | ColumnProperty.PrimaryKey)); + Provider.AddTable(tableName, new Column("Id", DbType.Int32, ColumnProperty.Unique | ColumnProperty.PrimaryKey)); // Act var columns = Provider.GetColumns(tableName); @@ -54,7 +55,7 @@ public void GetColumns_Primary_ColumnPropertyOk() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Id", System.Data.DbType.Int32, ColumnProperty.PrimaryKey)); + Provider.AddTable(tableName, new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey)); Provider.GetColumns(tableName); // Act @@ -73,8 +74,8 @@ public void GetColumns_PrimaryKeyOnTwoColumns_BothColumnsHavePrimaryKeyAndAreNot const string tableName = "GetColumnsTest"; Provider.AddTable(tableName, - new Column("Id", System.Data.DbType.Int32, ColumnProperty.PrimaryKey), - new Column("Id2", System.Data.DbType.Int32, ColumnProperty.PrimaryKey) + new Column("Id", DbType.Int32, ColumnProperty.PrimaryKey), + new Column("Id2", DbType.Int32, ColumnProperty.PrimaryKey) ); // Act @@ -86,13 +87,17 @@ public void GetColumns_PrimaryKeyOnTwoColumns_BothColumnsHavePrimaryKeyAndAreNot } [Test] - public void GetColumns_AddUniqueWithTwoColumns_NoUniqueOnColumnLevel() + public void GetColumns_AddUniqueConstraintWithTwoColumns_NoUniqueOnColumnLevel() { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Bla1", System.Data.DbType.Int32), new Column("Bla2", System.Data.DbType.Int32)); + const string column1Name = "Column1"; + const string column2Name = "Column2"; + const string constraintName = "ConstraintName"; - Provider.AddUniqueConstraint("IndexName", tableName, "Bla1", "Bla2"); + Provider.AddTable(tableName, new Column(column1Name, DbType.Int32), new Column(column2Name, DbType.Int32)); + + Provider.AddUniqueConstraint(constraintName, tableName, column1Name, column2Name); // Act var columns = Provider.GetColumns(tableName); @@ -106,7 +111,7 @@ public void GetSQLiteTableInfo_GetIndexesAndColumnsWithIndex_NoUniqueOnTheColumn { // Arrange const string tableName = "GetColumnsTest"; - Provider.AddTable(tableName, new Column("Bla1", System.Data.DbType.Int32), new Column("Bla2", System.Data.DbType.Int32)); + Provider.AddTable(tableName, new Column("Bla1", DbType.Int32), new Column("Bla2", DbType.Int32)); Provider.AddIndex("IndexName", tableName, ["Bla1", "Bla2"]); // Act diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetIndexesTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetIndexesTests.cs new file mode 100644 index 00000000..f55ab749 --- /dev/null +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_GetIndexesTests.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Migrator.Tests.Providers.Generic; +using NUnit.Framework; + +namespace Migrator.Tests.Providers.SQLite; + +[TestFixture] +[Category("SQLite")] +public class SQLiteTransformationProvider_GetIndexesTests : Generic_GetIndexesTestsBase +{ + [SetUp] + public async Task SetUpAsync() + { + await BeginSQLiteTransactionAsync(); + } +} From ff642bd5065299f97fda55f06483beb863c5f138 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 08:10:28 +0200 Subject: [PATCH 23/55] Added filtered/partial indexes to SQLite --- src/Migrator/Framework/Index.cs | 5 +- src/Migrator/Providers/Dialect.cs | 39 +++-- .../SQLite/SQLiteTransformationProvider.cs | 142 +++++++++++++++++- .../Providers/TransformationProvider.cs | 81 +--------- 4 files changed, 174 insertions(+), 93 deletions(-) diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index f310da70..e759bf77 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -1,4 +1,5 @@ -using DotNetProjects.Migrator.Providers.Models.Indexes; +using System.Collections.Generic; +using DotNetProjects.Migrator.Providers.Models.Indexes; namespace DotNetProjects.Migrator.Framework; @@ -21,5 +22,5 @@ public class Index : IDbField /// /// Gets or sets items that represent filter expressions in filtered indexes. Currently string, integer and boolean values are supported. /// - public FilterItem[] FilterItems { get; set; } = []; + public List FilterItems { get; set; } = []; } diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 34dd066e..4772ae03 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Data; using System.Globalization; +using System.Linq; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; namespace DotNetProjects.Migrator.Providers; @@ -17,6 +19,14 @@ public abstract class Dialect : IDialect private readonly TypeNames _typeNames = new(); private readonly List _unsignedCompatibleTypes = []; + private readonly List _filterTypeToStrings = [ + new() { FilterType = FilterType.EqualTo, FilterString = "=" }, + new() { FilterType = FilterType.GreaterThan, FilterString = ">" }, + new() { FilterType = FilterType.GreaterThanOrEqualTo, FilterString = ">=" }, + new() { FilterType = FilterType.SmallerThan, FilterString = "<" }, + new() { FilterType = FilterType.SmallerThanOrEqualTo, FilterString = "<=" } + ]; + protected Dialect() { RegisterProperty(ColumnProperty.Null, "NULL"); @@ -407,17 +417,26 @@ public ColumnPropertiesMapper GetAndMapColumnPropertiesWithoutDefault(Column col return mapper; } - public string GetComparisonStringFilterIndex(FilterType filterType) + public string GetComparisonStringByFilterType(FilterType filterType) { - return filterType switch - { - FilterType.EqualTo => "=", - FilterType.GreaterThan => ">", - FilterType.GreaterThanOrEqualTo => ">=", - FilterType.SmallerThan => "<", - FilterType.SmallerThanOrEqualTo => "<=", - _ => throw new NotImplementedException("Filter is not implemented yet."), - }; + var exceptionString = $"The {nameof(FilterType)} '{filterType}' is not implemented."; + var result = _filterTypeToStrings.FirstOrDefault(x => x.FilterType == filterType) ?? throw new NotImplementedException(exceptionString); + + return result.FilterString; + } + + /// + /// Resolves the comparison string for filtered indexes. + /// + /// + /// + /// + public FilterType GetFilterTypeByComparisonString(string comparisonString) + { + var exceptionString = $"The {comparisonString} cannot be resolved."; + var result = _filterTypeToStrings.FirstOrDefault(x => x.FilterString == comparisonString) ?? throw new Exception(exceptionString); + + return result.FilterType; } /// diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 932b69da..ba5ab7e2 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -10,6 +10,8 @@ using ForeignKeyConstraint = DotNetProjects.Migrator.Framework.ForeignKeyConstraint; using Index = DotNetProjects.Migrator.Framework.Index; using DotNetProjects.Migrator.Framework.Extensions; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; namespace DotNetProjects.Migrator.Providers.Impl.SQLite; @@ -656,7 +658,7 @@ public override bool PrimaryKeyExists(string table, string name) { var sqliteTableInfo = GetSQLiteTableInfo(table); - // SQLite does not offer named primary keys BUT since there can only be one primary key we return true if there is any PK. + // SQLite does not offer named primary keys BUT since there can only be one primary key per table we return true if there is any primary key. var hasPrimaryKey = sqliteTableInfo.Columns.Any(x => x.ColumnProperty.IsSet(ColumnProperty.PrimaryKey)); @@ -665,7 +667,18 @@ public override bool PrimaryKeyExists(string table, string name) public override void AddUniqueConstraint(string name, string table, params string[] columns) { + if (string.IsNullOrWhiteSpace(name)) + { + throw new MigrationException("Providing a constraint name is obligatory."); + } + var sqliteTableInfo = GetSQLiteTableInfo(table); + + if (sqliteTableInfo.Uniques.Any(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException("A unique constraint with the same name already exists."); + } + var uniqueConstraint = new Unique() { KeyColumns = columns, Name = name }; sqliteTableInfo.Uniques.Add(uniqueConstraint); @@ -1194,10 +1207,15 @@ public override bool IndexExists(string table, string name) public override Index[] GetIndexes(string table) { + var afterWhereRegex = new Regex("(?<= WHERE ).+"); List indexes = []; + var indexCreateScripts = GetCreateIndexSqlStrings(table); + var pragmaIndexListItems = GetPragmaIndexListItems(table).Where(x => x.Origin == "c"); + var columns = GetColumns(table); + foreach (var pragmaIndexListItem in pragmaIndexListItems) { var indexInfos = GetPragmaIndexInfo(pragmaIndexListItem.Name); @@ -1219,6 +1237,47 @@ public override Index[] GetIndexes(string table) Unique = pragmaIndexListItem.Unique }; + var script = indexCreateScripts.FirstOrDefault(x => x.Contains(pragmaIndexListItem.Name, StringComparison.OrdinalIgnoreCase)); + + if (script != null) + { + if (afterWhereRegex.Match(script) is Match match && match.Success) + { + var andSplitted = Regex.Split(match.Value, " AND "); + + var filterSingleStrings = andSplitted + .Select(x => x.Trim()) + .ToList(); + + foreach (var filterSingleString in filterSingleStrings) + { + var splitted = filterSingleString.Split(' ') + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .ToList(); + + var filterItem = new FilterItem { ColumnName = splitted[0], Filter = _dialect.GetFilterTypeByComparisonString(splitted[1]) }; + + var column = columns.Single(x => x.Name.Equals(splitted[0], StringComparison.OrdinalIgnoreCase)); + + filterItem.Value = column.MigratorDbType switch + { + MigratorDbType.Int16 => short.Parse(splitted[2]), + MigratorDbType.Int32 => int.Parse(splitted[2]), + MigratorDbType.Int64 => long.Parse(splitted[2]), + MigratorDbType.UInt16 => ushort.Parse(splitted[2]), + MigratorDbType.UInt32 => uint.Parse(splitted[2]), + MigratorDbType.UInt64 => ulong.Parse(splitted[2]), + MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), + _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + }; + + index.FilterItems.Add(filterItem); + } + } + } + indexes.Add(index); } @@ -1335,6 +1394,87 @@ public override void AddTable(string name, string engine, params IDbField[] fiel } } + public override void AddIndex(string table, Index index) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + foreach (var column in index.KeyColumns) + { + if (!ColumnExists(table, column)) + { + throw new MigrationException($"Column '{column}' does not exist."); + } + } + + if (IndexExists(table, index.Name)) + { + throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); + } + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + + if (hasIncludedColumns) + { + // This will be actived in the future. + // throw new MigrationException($"SQLite does not support included columns. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + } + + if (index.Clustered) + { + throw new MigrationException($"This migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + } + + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var columnsString = $"({string.Join(", ", columns)})"; + var filterString = string.Empty; + + if (index.FilterItems != null && index.FilterItems.Count > 0) + { + List singleFilterStrings = []; + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + value = filterItem.Value switch + { + bool booleanValue => booleanValue ? "1" : "0", + string stringValue => $"'{stringValue}'", + byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), + sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), + _ => throw new NotImplementedException("Given type is not implemented. Please file an issue."), + }; + + if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo) + { + throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with {nameof(FilterType.EqualTo)}."); + } + + var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; + + singleFilterStrings.Add(singleFilterString); + } + + filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; + } + + List list = ["CREATE", uniqueString, "INDEX", name, "ON", table, columnsString, filterString]; + + var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + + ExecuteNonQuery(sql); + } + protected override string GetPrimaryKeyConstraintName(string table) { throw new NotImplementedException(); diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 7027fc25..50cf1626 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -2092,86 +2092,7 @@ public virtual void RemoveIndex(string table, string name) public virtual void AddIndex(string table, Index index) { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - foreach (var column in index.KeyColumns) - { - if (!ColumnExists(table, column)) - { - throw new MigrationException($"Column '{column}' does not exist."); - } - } - - if (IndexExists(table, index.Name)) - { - throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); - } - - var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; - var name = QuoteConstraintNameIfRequired(index.Name); - table = QuoteTableNameIfRequired(table); - var columns = QuoteColumnNamesIfRequired(index.KeyColumns); - - var uniqueString = index.Unique ? "UNIQUE" : null; - var columnsString = $"({string.Join(", ", columns)})"; - var filterString = string.Empty; - - if (index.FilterItems != null && index.FilterItems.Length > 0) - { - List singleFilterStrings = []; - - foreach (var filterItem in index.FilterItems) - { - var comparisonString = _dialect.GetComparisonStringFilterIndex(filterItem.Filter); - - var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); - string value = null; - - if (filterItem.Value is bool booleanValue) - { - value = booleanValue ? "1" : "0"; - } - else if (filterItem.Value is string stringValue) - { - value = $"'{stringValue}'"; - } - else if (filterItem.Value is byte || filterItem.Value is short || filterItem.Value is int || filterItem.Value is long) - { - value = Convert.ToInt64(filterItem.Value).ToString(); - } - else if (filterItem.Value is sbyte || filterItem.Value is ushort || filterItem.Value is uint || filterItem.Value is ulong) - { - value = Convert.ToUInt64(filterItem.Value).ToString(); - } - else - { - throw new NotImplementedException("Given type is not implemented. Please file an issue."); - } - - var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; - - singleFilterStrings.Add(singleFilterString); - } - - filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; - } - - List list = []; - list.Add("CREATE"); - list.Add(uniqueString); - list.Add("INDEX"); - list.Add(name); - list.Add("ON"); - list.Add(table); - list.Add(columnsString); - list.Add(filterString); - - var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); - - ExecuteNonQuery(sql); + throw new NotImplementedException($"{nameof(AddIndex)} is not overridden for the provider."); } public virtual void AddIndex(string name, string table, params string[] columns) From eb0b4e06f299c4b0f68cbc9336fbe1f698f7f731 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 08:11:44 +0200 Subject: [PATCH 24/55] Tests for partial indexes --- .../Dialects/PostgreDialectTests.cs | 30 -------- .../Dialects/PostgreSQLDialectTests.cs | 43 +++++++++++ .../Generic/Generic_GetIndexesTestsBase.cs | 55 ++++++++++++++ ...SQLTransformationProvider_AddIndexTests.cs | 38 ++++++++++ .../PostgreSQLTransformationProvider.cs | 75 +++++++++++++++++++ .../SqlServerTransformationProvider.cs | 4 +- .../Providers/Models/FilterTypeToString.cs | 19 +++++ 7 files changed, 232 insertions(+), 32 deletions(-) delete mode 100644 src/Migrator.Tests/Dialects/PostgreDialectTests.cs create mode 100644 src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs create mode 100644 src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs create mode 100644 src/Migrator/Providers/Models/FilterTypeToString.cs diff --git a/src/Migrator.Tests/Dialects/PostgreDialectTests.cs b/src/Migrator.Tests/Dialects/PostgreDialectTests.cs deleted file mode 100644 index d8535f2d..00000000 --- a/src/Migrator.Tests/Dialects/PostgreDialectTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; -using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; -using NUnit.Framework; - -namespace Migrator.Tests.Dialects; - -[TestFixture] -[Category("Postgre")] -public class PostgreDialectTests -{ - private PostgreSQLDialect _postgreSQLDialect; - - [SetUp] - public void SetUp() - { - _postgreSQLDialect = new PostgreSQLDialect(); - } - - [TestCase(FilterType.EqualTo, "=")] - [TestCase(FilterType.GreaterThanOrEqualTo, ">=")] - [TestCase(FilterType.SmallerThanOrEqualTo, "<=")] - [TestCase(FilterType.SmallerThan, "<")] - [TestCase(FilterType.GreaterThan, ">")] - public void GetComparisonStringFilterIndex(FilterType filterType, string expectedString) - { - var result = _postgreSQLDialect.GetComparisonStringFilterIndex(filterType); - - Assert.That(result, Is.EqualTo(expectedString)); - } -} \ No newline at end of file diff --git a/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs b/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs new file mode 100644 index 00000000..b49e324a --- /dev/null +++ b/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs @@ -0,0 +1,43 @@ +using DotNetProjects.Migrator.Providers.Impl.PostgreSQL; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; +using NUnit.Framework; + +namespace Migrator.Tests.Dialects; + +[TestFixture] +[Category("Postgre")] +public class PostgreDialectTests +{ + private PostgreSQLDialect _postgreSQLDialect; + + [SetUp] + public void SetUp() + { + // Since Dialect is abstract we use PostgreSQLDialect + _postgreSQLDialect = new PostgreSQLDialect(); + } + + [TestCase(FilterType.EqualTo, "=")] + [TestCase(FilterType.GreaterThanOrEqualTo, ">=")] + [TestCase(FilterType.SmallerThanOrEqualTo, "<=")] + [TestCase(FilterType.SmallerThan, "<")] + [TestCase(FilterType.GreaterThan, ">")] + public void GetComparisonStringByFilterType_Success(FilterType filterType, string expectedString) + { + var result = _postgreSQLDialect.GetComparisonStringByFilterType(filterType); + + Assert.That(result, Is.EqualTo(expectedString)); + } + + [TestCase("=", FilterType.EqualTo)] + [TestCase(">=", FilterType.GreaterThanOrEqualTo)] + [TestCase("<=", FilterType.SmallerThanOrEqualTo)] + [TestCase("<", FilterType.SmallerThan)] + [TestCase(">", FilterType.GreaterThan)] + public void GetFilterTypeByComparisonString_Success(string comparisonString, FilterType expectedFilterType) + { + var result = _postgreSQLDialect.GetFilterTypeByComparisonString(comparisonString); + + Assert.That(result, Is.EqualTo(expectedFilterType)); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs new file mode 100644 index 00000000..eefd0764 --- /dev/null +++ b/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs @@ -0,0 +1,55 @@ +using System.Data; +using System.Linq; +using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; +using Migrator.Tests.Providers.Base; +using NUnit.Framework; +using Index = DotNetProjects.Migrator.Framework.Index; + +namespace Migrator.Tests.Providers.Generic; + +public abstract class Generic_GetIndexesTestsBase : TransformationProviderBase +{ + [Test] + public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName, DbType.Int32), + new Column(columnName2, DbType.String), + new Column(columnName3, DbType.Int32) + ); + + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName, columnName2], + Unique = true, + FilterItems = [ + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName, Value = 100 }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName2, Value = "Hello" }, + ] + }); + + // Act + var indexes = Provider.GetIndexes(table: tableName); + + var index = indexes.Single(); + + var filterItem1 = index.FilterItems.Single(x => x.ColumnName == columnName); + var filterItem2 = index.FilterItems.Single(x => x.ColumnName == columnName2); + + Assert.That(filterItem1.Filter, Is.EqualTo(FilterType.GreaterThanOrEqualTo)); + Assert.That((int)filterItem1.Value, Is.EqualTo(100)); + + Assert.That(filterItem1.Filter, Is.EqualTo(FilterType.EqualTo)); + Assert.That((string)filterItem1.Value, Is.EqualTo("Hello")); + } +} \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index f5f5925b..c1a70d8f 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using Npgsql; using NUnit.Framework; @@ -61,4 +62,41 @@ public void AddIndex_Unique_Success() Assert.That(ex.Message, Does.StartWith("23505: duplicate key value violates unique constraint")); Assert.That(ex.SqlState, Is.EqualTo("23505")); } + + [Test] + public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName, columnName2], + Unique = true, + FilterItems = [ + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName, Value = 100 }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName2, Value = "Hello" }, + ] + }); + + // Assert + var index = Provider.GetIndexes(tableName).Single(); + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + // Unique but no exception is thrown since smaller than 100 + Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); + + Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"]); + var sqliteException = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"])); + + Assert.That(index.Unique, Is.True); + Assert.That(sqliteException.SqlState, Is.EqualTo("23505")); + } } diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index 34f04aee..6016ac7c 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -12,6 +12,7 @@ #endregion using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; using System; using System.Collections.Generic; using System.Data; @@ -54,9 +55,83 @@ protected override string GetPrimaryKeyConstraintName(string table) using var cmd = CreateCommand(); using var reader = ExecuteQuery(cmd, string.Format("SELECT conname FROM pg_constraint WHERE contype = 'p' AND conrelid = (SELECT oid FROM pg_class WHERE relname = lower('{0}'));", table)); + return reader.Read() ? reader.GetString(0) : null; } + public override void AddIndex(string table, Index index) + { + if (!TableExists(table)) + { + throw new MigrationException($"Table '{table}' does not exist."); + } + + foreach (var column in index.KeyColumns) + { + if (!ColumnExists(table, column)) + { + throw new MigrationException($"Column '{column}' does not exist."); + } + } + + if (IndexExists(table, index.Name)) + { + throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); + } + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var columnsString = $"({string.Join(", ", columns)})"; + var filterString = string.Empty; + + if (index.FilterItems != null && index.FilterItems.Count > 0) + { + List singleFilterStrings = []; + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + value = filterItem.Value switch + { + bool booleanValue => booleanValue ? "TRUE" : "FALSE", + string stringValue => $"'{stringValue}'", + byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), + sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), + _ => throw new NotImplementedException($"Given type in '{nameof(FilterItem)}' is not implemented. Please file an issue."), + }; + + var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; + + singleFilterStrings.Add(singleFilterString); + } + + filterString = $"WHERE {string.Join(" AND ", singleFilterStrings)}"; + } + + List list = []; + list.Add("CREATE"); + list.Add(uniqueString); + list.Add("INDEX"); + list.Add(name); + list.Add("ON"); + list.Add(table); + list.Add(columnsString); + list.Add(filterString); + + var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); + + ExecuteNonQuery(sql); + } + + public override Index[] GetIndexes(string table) { var retVal = new List(); diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 60954e81..29795615 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -173,13 +173,13 @@ public override void AddIndex(string table, Index index) var filterString = string.Empty; var clusteredString = index.Clustered ? "CLUSTERED" : "NONCLUSTERED"; - if (index.FilterItems != null && index.FilterItems.Length > 0) + if (index.FilterItems != null && index.FilterItems.Count > 0) { List singleFilterStrings = []; foreach (var filterItem in index.FilterItems) { - var comparisonString = _dialect.GetComparisonStringFilterIndex(filterItem.Filter); + var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); string value = null; diff --git a/src/Migrator/Providers/Models/FilterTypeToString.cs b/src/Migrator/Providers/Models/FilterTypeToString.cs new file mode 100644 index 00000000..8b4adb0e --- /dev/null +++ b/src/Migrator/Providers/Models/FilterTypeToString.cs @@ -0,0 +1,19 @@ +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; + +namespace DotNetProjects.Migrator.Providers.Models; + +/// +/// Model for filter type => filter string mapping. +/// +public class FilterTypeToString +{ + /// + /// Gets or sets the filter type + /// + public FilterType FilterType { get; set; } + + /// + /// Gets or sets the filter string like >, <, =, >= etc. + /// + public string FilterString { get; set; } +} \ No newline at end of file From 5d32ebd39e7ed209090126ef1721f01dcd2d5f0f Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 18:13:55 +0200 Subject: [PATCH 25/55] Added validation of index instance --- .../Providers/TransformationProvider.cs | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 50cf1626..3199c40d 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -16,8 +16,6 @@ using DotNetProjects.Migrator.Framework.SchemaBuilder; using DotNetProjects.Migrator.Providers.Impl.SQLite; using DotNetProjects.Migrator.Providers.Models; -using DotNetProjects.Migrator.Providers.Models.Indexes; -using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using System; using System.Collections.Generic; using System.Data; @@ -41,7 +39,7 @@ public abstract class TransformationProvider : ITransformationProvider private string _scope; protected readonly string _connectionString; protected readonly string _defaultSchema; - private readonly ForeignKeyConstraintMapper constraintMapper = new ForeignKeyConstraintMapper(); + private readonly ForeignKeyConstraintMapper constraintMapper = new(); protected List _appliedMigrations; protected IDbConnection _connection; protected bool _outsideConnection = false; @@ -2189,5 +2187,43 @@ public IEnumerable GetColumns(string schema, string table) return from DataRow row in tables.Rows select (row["TABLE_NAME"] as string); } + protected void ValidateIndex(string tableName, Index index) + { + var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; + var columns = GetColumns(table: tableName); + + if (!TableExists(tableName)) + { + throw new MigrationException($"Table '{tableName}' does not exist."); + } + + foreach (var keyColumn in index.KeyColumns) + { + if (!columns.Any(x => x.Name.Equals(keyColumn, StringComparison.OrdinalIgnoreCase))) + { + throw new MigrationException($"Column '{keyColumn}' does not exist."); + } + } + if (hasFilterItems) + { + if (!index.KeyColumns.Any(x => index.FilterItems.Any(y => x.Equals(y.ColumnName, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"All columns in the {index.FilterItems} should exist in the {index.KeyColumns}."); + } + } + + if (IndexExists(tableName, index.Name)) + { + throw new MigrationException($"Index '{index.Name}' in table {tableName} already exists."); + } + + if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) + { + if (index.IncludeColumns.Any(x => index.KeyColumns.Any(y => x.Equals(y, StringComparison.OrdinalIgnoreCase)))) + { + throw new MigrationException($"It is not allowed to use a column in {nameof(index.IncludeColumns)} that exist in {nameof(index.KeyColumns)}."); + } + } + } } From c4c867f8d7345a35822b9ff70ee241695be7a8d4 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 18:14:12 +0200 Subject: [PATCH 26/55] Updated documentation of index --- src/Migrator/Framework/Index.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index e759bf77..5376a944 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers; namespace DotNetProjects.Migrator.Framework; @@ -9,18 +10,35 @@ public class Index : IDbField public bool Unique { get; set; } + /// + /// Indicates whether the index is clustered (false for NONCLUSTERED). + /// Please mind that this is ignored in Oracle and SQLite (supported in SQLite but not in this migrator) + /// public bool Clustered { get; set; } - public bool PrimaryKey { get; set; } + /// + /// Indicates whether it is a primary key constraint. If you want to set a primary key use in + /// + public bool PrimaryKey { get; internal set; } - public bool UniqueConstraint { get; set; } + /// + /// Indicates whether it is a unique constraint. If you want to set a unique constraint use the method + /// + public bool UniqueConstraint { get; internal set; } + /// + /// Gets or sets the column names in the index (not included columns). + /// public string[] KeyColumns { get; set; } = []; + /// + /// Gets or sets the included columns. Not supported in SQLite and Oracle. + /// public string[] IncludeColumns { get; set; } = []; /// /// Gets or sets items that represent filter expressions in filtered indexes. Currently string, integer and boolean values are supported. + /// Attention: In SQL Server the column used in the filter must be NOT NULL. /// public List FilterItems { get; set; } = []; } From 60278bc15ed5e2ff6ad6c602a5c25a5c9ff6ff08 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 18:14:29 +0200 Subject: [PATCH 27/55] Added GetComparisonStrings --- src/Migrator/Providers/Dialect.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index 4772ae03..bd83f5e4 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -425,6 +425,11 @@ public string GetComparisonStringByFilterType(FilterType filterType) return result.FilterString; } + public string[] GetComparisonStrings() + { + return _filterTypeToStrings.Select(x => x.FilterString).ToArray(); + } + /// /// Resolves the comparison string for filtered indexes. /// From ab5377903bf0747a943fcf7c56006e989d6270c5 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 18:14:44 +0200 Subject: [PATCH 28/55] Minor changes --- .../Providers/Impl/Informix/InformixTransformationProvider.cs | 1 - .../Providers/Impl/Ingres/IngresTransformationProvider.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs b/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs index b8657821..4ab49fa7 100644 --- a/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Informix/InformixTransformationProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data; -using DotNetProjects.Migrator.Providers; namespace DotNetProjects.Migrator.Providers.Impl.Informix; diff --git a/src/Migrator/Providers/Impl/Ingres/IngresTransformationProvider.cs b/src/Migrator/Providers/Impl/Ingres/IngresTransformationProvider.cs index 80c50d04..ac69a823 100644 --- a/src/Migrator/Providers/Impl/Ingres/IngresTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Ingres/IngresTransformationProvider.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Data; -using DotNetProjects.Migrator.Providers; namespace DotNetProjects.Migrator.Providers.Impl.Ingres; From 30ab79f00b416e9ad975b99969670847ae64ed5c Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Mon, 25 Aug 2025 18:15:59 +0200 Subject: [PATCH 29/55] Replaced old getindex in postgre. New includes "include" and "filtered" --- ...SQLTransformationProvider_AddIndexTests.cs | 65 +++++- .../PostgreSQLTransformationProvider.cs | 197 +++++++++++++----- .../SQLite/SQLiteTransformationProvider.cs | 20 +- .../SqlServerTransformationProvider.cs | 24 +-- 4 files changed, 212 insertions(+), 94 deletions(-) diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index c1a70d8f..7f4dc32a 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Data; using System.Linq; using System.Threading.Tasks; @@ -94,9 +95,69 @@ public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() Provider.Insert(tableName, [columnName, columnName2], [1, "Hello"]); Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"]); - var sqliteException = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"])); + var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName, columnName2], [100, "Hello"])); Assert.That(index.Unique, Is.True); - Assert.That(sqliteException.SqlState, Is.EqualTo("23505")); + Assert.That(ex.SqlState, Is.EqualTo("23505")); + } + + [Test] + public void AddIndex_IncludeColumnsSingle_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + IncludeColumns = [columnName2] + }); + + // Assert + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(index.KeyColumns.Single, Is.EqualTo(columnName).IgnoreCase); + Assert.That(index.IncludeColumns.Single, Is.EqualTo(columnName2).IgnoreCase); + } + + [Test] + public void AddIndex_IncludeColumnsMultiple_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String), new Column(columnName3, DbType.Boolean)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + IncludeColumns = [columnName2, columnName3] + }); + + // Assert + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(index.KeyColumns.Single, Is.EqualTo(columnName).IgnoreCase); + Assert.That(index.IncludeColumns, Is.EquivalentTo([columnName2, columnName3]) + .Using((x, y) => string.Compare(x, y, ignoreCase: true))); } } diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index 6016ac7c..92093c91 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -13,6 +13,7 @@ using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using System; using System.Collections.Generic; using System.Data; @@ -61,23 +62,7 @@ protected override string GetPrimaryKeyConstraintName(string table) public override void AddIndex(string table, Index index) { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - foreach (var column in index.KeyColumns) - { - if (!ColumnExists(table, column)) - { - throw new MigrationException($"Column '{column}' does not exist."); - } - } - - if (IndexExists(table, index.Name)) - { - throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); - } + ValidateIndex(tableName: table, index: index); var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; var name = QuoteConstraintNameIfRequired(index.Name); @@ -87,6 +72,12 @@ public override void AddIndex(string table, Index index) var uniqueString = index.Unique ? "UNIQUE" : null; var columnsString = $"({string.Join(", ", columns)})"; var filterString = string.Empty; + var includeString = string.Empty; + + if (index.IncludeColumns != null && index.IncludeColumns.Length > 0) + { + includeString = $"INCLUDE ({string.Join(", ", index.IncludeColumns)})"; + } if (index.FilterItems != null && index.FilterItems.Count > 0) { @@ -125,6 +116,7 @@ public override void AddIndex(string table, Index index) list.Add(table); list.Add(columnsString); list.Add(filterString); + list.Add(includeString); var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); @@ -134,53 +126,156 @@ public override void AddIndex(string table, Index index) public override Index[] GetIndexes(string table) { - var retVal = new List(); + var columns = GetColumns(table); + + // Since the migrator does not support schemas at this point in time we set the schema to "public" + var schemaName = "public"; - var sql = @" -SELECT * FROM ( -SELECT i.relname as indname, - idx.indisprimary, - idx.indisunique, - idx.indisclustered, - i.relowner as indowner, - cast(idx.indrelid::regclass as varchar) as tablenm, - am.amname as indam, - idx.indkey, - ARRAY_TO_STRING(ARRAY( - SELECT pg_get_indexdef(idx.indexrelid, k + 1, true) - FROM generate_subscripts(idx.indkey, 1) as k - ORDER BY k - ), ',') as indkey_names, - idx.indexprs IS NOT NULL as indexprs, - idx.indpred IS NOT NULL as indpred -FROM pg_index as idx -JOIN pg_class as i -ON i.oid = idx.indexrelid -JOIN pg_am as am -ON i.relam = am.oid -JOIN pg_namespace as ns -ON ns.oid = i.relnamespace -AND ns.nspname = ANY(current_schemas(false))) AS t -WHERE lower(tablenm) = lower('{0}') -;"; + var retVal = new List(); + var sql = @$" + SELECT + nsp.nspname AS schema_name, + tbl.relname AS table_name, + cls.relname AS index_name, + idx.indisunique AS is_unique, + idx.indisclustered AS is_clustered, + con.contype = 'u' AS is_unique_constraint, + con.contype = 'p' AS is_primary_constraint, + pg_get_indexdef(idx.indexrelid) AS index_definition, + ( + SELECT string_agg(att.attname, ', ') + FROM unnest(idx.indkey) WITH ORDINALITY AS cols(attnum, ord) + JOIN pg_attribute att + ON att.attrelid = idx.indrelid + AND att.attnum = cols.attnum + WHERE cols.ord <= idx.indnkeyatts + ) AS index_columns, + ( + SELECT string_agg(att.attname, ', ') + FROM unnest(idx.indkey) WITH ORDINALITY AS cols(attnum, ord) + JOIN pg_attribute att + ON att.attrelid = idx.indrelid + AND att.attnum = cols.attnum + WHERE cols.ord > idx.indnkeyatts + ) AS include_columns, + pg_get_expr(idx.indpred, idx.indrelid) AS partial_filter + FROM pg_index idx + JOIN pg_class cls ON cls.oid = idx.indexrelid + JOIN pg_class tbl ON tbl.oid = idx.indrelid + JOIN pg_namespace nsp ON nsp.oid = tbl.relnamespace + LEFT JOIN pg_constraint con ON con.conindid = idx.indexrelid + WHERE + lower(tbl.relname) = '{table.ToLowerInvariant()}' AND + nsp.nspname = '{schemaName}'"; using (var cmd = CreateCommand()) using (var reader = ExecuteQuery(cmd, string.Format(sql, table))) { + var schemaNameOrdinal = reader.GetOrdinal("schema_name"); + var tableNameOrdinal = reader.GetOrdinal("table_name"); + var indexNameOrdinal = reader.GetOrdinal("index_name"); + var isUniqueOrdinal = reader.GetOrdinal("is_unique"); + var isClusteredOrdinal = reader.GetOrdinal("is_clustered"); + var isUniqueConstraintOrdinal = reader.GetOrdinal("is_unique_constraint"); + var isPrimaryConstraintOrdinal = reader.GetOrdinal("is_primary_constraint"); + var indexDefinitionOrdinal = reader.GetOrdinal("index_definition"); + var indexColumnsOrdinal = reader.GetOrdinal("index_columns"); + var includeColumnsOrdinal = reader.GetOrdinal("include_columns"); + var partialFilterOrdinal = reader.GetOrdinal("partial_filter"); + while (reader.Read()) { if (!reader.IsDBNull(1)) { + var indexDefinition = reader.GetString(indexDefinitionOrdinal); + var indexColumns = !reader.IsDBNull(indexColumnsOrdinal) ? reader.GetString(indexColumnsOrdinal) : null; + var partialColumns = !reader.IsDBNull(partialFilterOrdinal) ? reader.GetString(partialFilterOrdinal) : null; + var includeColumns = !reader.IsDBNull(includeColumnsOrdinal) ? reader.GetString(includeColumnsOrdinal) : null; + + if (!string.IsNullOrWhiteSpace(partialColumns)) + { + partialColumns = partialColumns.Substring(1, partialColumns.Length - 2); + var partialSplitted = Regex.Split(partialColumns, " AND ").Select(x => x.Trim()).Select(x => x.Substring(1, x.Length - 2)).ToList(); + + var comparisonStrings = _dialect.GetComparisonStrings(); + + List filterItems = []; + + foreach (var partialItemString in partialSplitted) + { + string[] splits = []; + var filterType = FilterType.None; + + foreach (var comparisonString in comparisonStrings) + { + splits = Regex.Split(partialItemString, $" {comparisonString} "); + + if (splits.Length == 2) + { + filterType = _dialect.GetFilterTypeByComparisonString(comparisonString); + break; + } + } + + if (splits.Length != 2) + { + throw new NotImplementedException($"Comparison string in '{partialItemString}'"); + } + + var columnNameString = splits[0]; + + var columnNameRegex = new Regex(@"(?<=^\().+(?=\)::(text|boolean|integer)$)"); + if (columnNameRegex.Match(columnNameString) is Match matchColumnName && matchColumnName.Success) + { + columnNameString = matchColumnName.Value; + } + + var column = columns.First(x => columnNameString.Equals(x.Name, StringComparison.OrdinalIgnoreCase)); + + var stringValueRegex = new Regex("(?<=^').+(?='::(text|boolean|integer)$)"); + + var valueAsString = splits[1]; + + if (stringValueRegex.Match(splits[1]) is Match match && match.Success) + { + valueAsString = match.Value; + } + + var filterItem = new FilterItem + { + ColumnName = column.Name, + Filter = filterType, + }; + + filterItem.Value = column.MigratorDbType switch + { + MigratorDbType.Int16 => short.Parse(valueAsString), + MigratorDbType.Int32 => int.Parse(valueAsString), + MigratorDbType.Int64 => long.Parse(valueAsString), + MigratorDbType.UInt16 => ushort.Parse(valueAsString), + MigratorDbType.UInt32 => uint.Parse(valueAsString), + MigratorDbType.UInt64 => ulong.Parse(valueAsString), + MigratorDbType.Boolean => valueAsString == "1" || valueAsString.Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => valueAsString, + _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + }; + + filterItems.Add(filterItem); + } + } + var idx = new Index { - Name = reader.GetString(0), - PrimaryKey = reader.GetBoolean(1), - Unique = reader.GetBoolean(2), - Clustered = reader.GetBoolean(3), + Name = reader.GetString(indexNameOrdinal), + PrimaryKey = !reader.IsDBNull(isPrimaryConstraintOrdinal) && reader.GetBoolean(isPrimaryConstraintOrdinal), + Unique = !reader.IsDBNull(isUniqueOrdinal) && reader.GetBoolean(isUniqueOrdinal), + UniqueConstraint = !reader.IsDBNull(isUniqueConstraintOrdinal) && reader.GetBoolean(isUniqueConstraintOrdinal), + Clustered = !reader.IsDBNull(isClusteredOrdinal) && reader.GetBoolean(isClusteredOrdinal), + IncludeColumns = !string.IsNullOrWhiteSpace(includeColumns) ? [.. includeColumns.Split(',').Select(x => x.Trim())] : null, + KeyColumns = !string.IsNullOrWhiteSpace(indexColumns) ? [.. indexColumns.Split(',').Select(x => x.Trim())] : null, }; - var cols = reader.GetString(8); - idx.KeyColumns = cols.Split(','); + retVal.Add(idx); } } diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index ba5ab7e2..6da22847 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1396,23 +1396,7 @@ public override void AddTable(string name, string engine, params IDbField[] fiel public override void AddIndex(string table, Index index) { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - foreach (var column in index.KeyColumns) - { - if (!ColumnExists(table, column)) - { - throw new MigrationException($"Column '{column}' does not exist."); - } - } - - if (IndexExists(table, index.Name)) - { - throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); - } + ValidateIndex(table, index); var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; @@ -1424,7 +1408,7 @@ public override void AddIndex(string table, Index index) if (index.Clustered) { - throw new MigrationException($"This migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); + throw new MigrationException($"For SQLite this migrator does not support clustered indexes at this point in time, sorry. File an issue if needed. Use 'if(Provider is {nameof(SQLiteTransformationProvider)}' if necessary."); } var name = QuoteConstraintNameIfRequired(index.Name); diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 29795615..1a7297c1 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -137,31 +137,9 @@ public override void AddPrimaryKeyNonClustered(string name, string table, params string.Join(",", QuoteColumnNamesIfRequired(columns)))); } - public override void AddIndex(string name, string table, params string[] columns) - { - var index = new Index { Name = name, KeyColumns = columns }; - AddIndex(table, index); - } - public override void AddIndex(string table, Index index) { - if (!TableExists(table)) - { - throw new MigrationException($"Table '{table}' does not exist."); - } - - foreach (var column in index.KeyColumns) - { - if (!ColumnExists(table, column)) - { - throw new MigrationException($"Column '{column}' does not exist."); - } - } - - if (IndexExists(table, index.Name)) - { - throw new MigrationException($"Index '{index.Name}' in table {table} already exists"); - } + ValidateIndex(tableName: table, index: index); var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; var name = QuoteConstraintNameIfRequired(index.Name); From 7c8028368d53eed079d953b05970154fec12c5a2 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Tue, 26 Aug 2025 14:08:52 +0200 Subject: [PATCH 30/55] SQLServer added FilterItems - partial index --- ...iteTransformationProvider_AddTableTests.cs | 6 +- .../SqlServerTransformationProvider.cs | 214 +++++++++++++----- .../Providers/Models/Indexes/IndexItem.cs | 71 ++++++ 3 files changed, 237 insertions(+), 54 deletions(-) create mode 100644 src/Migrator/Providers/Models/Indexes/IndexItem.cs diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index 3cea2832..af0fb356 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -1,4 +1,5 @@ using System; +using System.Data.SQLite; using System.Linq; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.SQLite; @@ -44,7 +45,7 @@ public void AddTable_CompositePrimaryKey_ContainsNull() ); Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); - Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); + var ex = Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); @@ -57,6 +58,9 @@ public void AddTable_CompositePrimaryKey_ContainsNull() var sqliteInfo = ((SQLiteTransformationProvider)Provider).GetSQLiteTableInfo(tableName); Assert.That(sqliteInfo.Columns.First().Name, Is.EqualTo(columnName1)); Assert.That(sqliteInfo.Columns[1].Name, Is.EqualTo(columnName2)); + + // 19 = UNIQUE constraint failed + Assert.That(ex.ErrorCode, Is.EqualTo(19)); } [Test] diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 1a7297c1..fa0953e2 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -12,6 +12,7 @@ #endregion using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; using System; using System.Collections.Generic; using System.Data; @@ -275,75 +276,182 @@ public override void RemoveColumnDefaultValue(string table, string column) public override Index[] GetIndexes(string table) { - var retVal = new List(); - - var sql = @"SELECT Tab.[name] AS TableName, - Ind.[name] AS IndexName, - Ind.[type_desc] AS IndexType, - Ind.[is_primary_key] AS IndexPrimary, - Ind.[is_unique] AS IndexUnique, - Ind.[is_unique_constraint] AS ConstraintUnique, - SUBSTRING(( SELECT ',' + AC.name - FROM sys.[tables] AS T - INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id] - INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] - AND I.[index_id] = IC.[index_id] - INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] - AND IC.[column_id] = AC.[column_id] - WHERE Ind.[object_id] = I.[object_id] - AND Ind.index_id = I.index_id - AND IC.is_included_column = 0 - ORDER BY IC.key_ordinal - FOR - XML PATH('') ), 2, 8000) AS KeyCols, - SUBSTRING(( SELECT ',' + AC.name - FROM sys.[tables] AS T - INNER JOIN sys.[indexes] I ON T.[object_id] = I.[object_id] - INNER JOIN sys.[index_columns] IC ON I.[object_id] = IC.[object_id] - AND I.[index_id] = IC.[index_id] - INNER JOIN sys.[all_columns] AC ON T.[object_id] = AC.[object_id] - AND IC.[column_id] = AC.[column_id] - WHERE Ind.[object_id] = I.[object_id] - AND Ind.index_id = I.index_id - AND IC.is_included_column = 1 - ORDER BY IC.key_ordinal - FOR - XML PATH('') ), 2, 8000) AS IncludeCols -FROM sys.[indexes] Ind - INNER JOIN sys.[tables] AS Tab ON Tab.[object_id] = Ind.[object_id] - WHERE LOWER(Tab.[name]) = LOWER('{0}')"; + // This migrator does not support schemas so we fall back to dbo in SQL Server + var schemaName = "dbo"; + + var indexes = new List(); + + var sql = @$"SELECT + s.name AS SchemaName, + t.name AS TableName, + i.name AS IndexName, + i.type_desc AS IndexType, + i.is_unique AS IsUnique, + i.is_primary_key AS IsPrimaryKey, + i.is_unique_constraint AS IsUniqueConstraint, + ic.index_column_id AS ColumnOrder, + col.name AS ColumnName, + ic.is_descending_key AS IsDescending, + ic.is_included_column AS IsIncludedColumn, + i.has_filter AS IsFilteredIndex, + i.filter_definition AS FilterDefinition + FROM + sys.indexes i + JOIN sys.tables t ON i.object_id = t.object_id + JOIN sys.schemas s ON t.schema_id = s.schema_id + JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id + JOIN sys.columns col ON ic.object_id = col.object_id AND ic.column_id = col.column_id + WHERE + LOWER(t.name) = '{table.ToLowerInvariant()}' AND + LOWER(s.name) = '{schemaName.ToLowerInvariant()}' + ORDER BY + s.name, t.name, i.name, ic.index_column_id"; + + List indexItems = []; using (var cmd = CreateCommand()) using (var reader = ExecuteQuery(cmd, string.Format(sql, table))) { + var columnNameOrdinal = reader.GetOrdinal("ColumnName"); + var columnOrderOrdinal = reader.GetOrdinal("ColumnOrder"); + var filterDefinitionOrdinal = reader.GetOrdinal("FilterDefinition"); + var indexNameOrdinal = reader.GetOrdinal("IndexName"); + var indexTypeOrdinal = reader.GetOrdinal("IndexType"); + var isDescendingOrdinal = reader.GetOrdinal("IsDescending"); + var isFilteredIndexOrdinal = reader.GetOrdinal("IsFilteredIndex"); + var isIncludedColumnOrdinal = reader.GetOrdinal("IsIncludedColumn"); + var isPrimaryKeyOrdinal = reader.GetOrdinal("IsPrimaryKey"); + var isUniqueConstraintOrdinal = reader.GetOrdinal("IsUniqueConstraint"); + var isUniqueOrdinal = reader.GetOrdinal("IsUnique"); + var schemaNameOrdinal = reader.GetOrdinal("SchemaName"); + var tableNameOrdinal = reader.GetOrdinal("TableName"); + + + while (reader.Read()) { - if (!reader.IsDBNull(1)) + var indexItem = new IndexItem { - var idx = new Index - { - Name = reader.GetString(1), - Clustered = reader.GetString(2) == "CLUSTERED", - PrimaryKey = reader.GetBoolean(3), - Unique = reader.GetBoolean(4), - UniqueConstraint = reader.GetBoolean(5), - }; + Clustered = reader.GetString(indexTypeOrdinal) == "CLUSTERED", + ColumnName = reader.GetString(columnNameOrdinal), + FilterString = reader.GetString(filterDefinitionOrdinal), + IsFilteredIndex = reader.GetBoolean(isFilteredIndexOrdinal), + IsIncludedColumn = reader.GetBoolean(isIncludedColumnOrdinal), + Name = reader.GetString(indexNameOrdinal), + PrimaryKey = reader.GetBoolean(isPrimaryKeyOrdinal), + SchemaName = reader.GetString(schemaNameOrdinal), + TableName = reader.GetString(tableNameOrdinal), + Unique = reader.GetBoolean(isUniqueOrdinal), + UniqueConstraint = reader.GetBoolean(isUniqueConstraintOrdinal), + }; + + indexItems.Add(indexItem); + } + } + + var indexGroups = indexItems.GroupBy(x => new + { + x.Name, + x.SchemaName, + x.TableName, + }); + + foreach (var indexGroup in indexGroups) + { + var first = indexGroup.First(); + + List filterItems = []; + + if (!string.IsNullOrWhiteSpace(first.FilterString)) + { + const string unexpectedPatternString = "Unexpected pattern in filter string detected. Not implemented yet - please file an issue"; + var comparisonStrings = _dialect.GetComparisonStrings(); + var stripOuterBracesRegex = new Regex(@"(?<=^\().+(?=\)$)"); + var stripBracesMatch = stripOuterBracesRegex.Match(first.FilterString.Trim()); + + if (!stripBracesMatch.Success) + { + throw new NotImplementedException(unexpectedPatternString); + } + + var andSplitted = Regex.Split(stripBracesMatch.Value, @" AND (?=\[)") + .Select(x => x.Trim()) + .ToList(); + + var columns = GetColumns(table: table); + + foreach (var andSplittedItem in andSplitted) + { + var filterItem = new FilterItem(); + // We assume nobody uses column names with brackets in it. + var columnRegex = new Regex(@"(?<=^\[)[^\]]+"); + var columnMatch = columnRegex.Match(andSplittedItem); - if (!reader.IsDBNull(6)) + if (!columnMatch.Success) { - idx.KeyColumns = reader.GetString(6).Split(','); + throw new NotImplementedException(unexpectedPatternString); } - if (!reader.IsDBNull(7)) + + filterItem.ColumnName = columnMatch.Value; + var column = columns.OrderByDescending(x => x.Name).First(x => x.Name.Equals(filterItem.ColumnName, StringComparison.OrdinalIgnoreCase)); + + var remainingString = andSplittedItem.Substring(filterItem.ColumnName.Length + 2); + var comparisonString = comparisonStrings.OrderByDescending(x => x.Length) + .First(x => remainingString.StartsWith(x)); + + filterItem.Filter = _dialect.GetFilterTypeByComparisonString(comparisonString); + remainingString = remainingString.Substring(comparisonString.Length); + + var valueRegex = new Regex(@"(?<=^[\(|']).+(?=[\)|']$)"); + var valueStringMatch = valueRegex.Match(remainingString); + + if (!valueStringMatch.Success) { - idx.IncludeColumns = reader.GetString(7).Split(','); + throw new NotImplementedException(unexpectedPatternString); } - retVal.Add(idx); + var valueAsString = valueStringMatch.Value; + + filterItem.Value = column.MigratorDbType switch + { + MigratorDbType.Int16 => short.Parse(valueAsString), + MigratorDbType.Int32 => int.Parse(valueAsString), + MigratorDbType.Int64 => long.Parse(valueAsString), + MigratorDbType.UInt16 => ushort.Parse(valueAsString), + MigratorDbType.UInt32 => uint.Parse(valueAsString), + MigratorDbType.UInt64 => ulong.Parse(valueAsString), + MigratorDbType.Boolean => valueAsString == "1" || valueAsString.Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => valueAsString, + _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + }; + + filterItems.Add(filterItem); } } - } - return retVal.ToArray(); + var index = new Index + { + Clustered = first.Clustered, + FilterItems = filterItems, + IncludeColumns = [.. indexGroup.Where(x => x.IsIncludedColumn) + .OrderBy(x => x.ColumnOrder) + .Select(x => x.ColumnName) + .Distinct()], + KeyColumns = [.. indexGroup.Where(x => !x.IsIncludedColumn) + .OrderBy(x => x.ColumnOrder) + .Select(x => x.ColumnName) + .Distinct()], + Name = first.Name, + PrimaryKey = first.PrimaryKey, + Unique = first.Unique, + UniqueConstraint = first.UniqueConstraint, + }; + + indexes.Add(index); + + } + + return [.. indexes]; } public override int GetColumnContentSize(string table, string columnName) diff --git a/src/Migrator/Providers/Models/Indexes/IndexItem.cs b/src/Migrator/Providers/Models/Indexes/IndexItem.cs new file mode 100644 index 00000000..a392a57f --- /dev/null +++ b/src/Migrator/Providers/Models/Indexes/IndexItem.cs @@ -0,0 +1,71 @@ +namespace DotNetProjects.Migrator.Providers.Models.Indexes; + +public class IndexItem +{ + /// + /// Indicates whether the index is clustered (false for NONCLUSTERED). + /// + public bool Clustered { get; set; } + + /// + /// Gets or sets the column order. + /// + public int ColumnOrder { get; set; } + + + + + /// + /// Gets or sets the index name. + /// + public string Name { get; set; } + + /// + /// Indicates whether the index is unique. + /// + public bool Unique { get; set; } + + + + /// + /// Indicates whether it is a primary key constraint. + /// + public bool PrimaryKey { get; internal set; } + + /// + /// Indicates whether it is a unique constraint. + /// + public bool UniqueConstraint { get; internal set; } + + /// + /// Gets or sets the column name. + /// + public string ColumnName { get; set; } + + /// + /// Gets or sets the included columns. Not supported in SQLite and Oracle. + /// + public bool IsIncludedColumn { get; set; } + + /// + /// Indicates whether the index is a filtered index. + /// + public bool IsFilteredIndex { get; set; } + + /// + /// Gets or sets items that represent filter expressions in filtered indexes. Currently string, integer and boolean values are supported. + /// Attention: In SQL Server the column used in the filter must be NOT NULL. + /// + public string FilterString { get; set; } + + /// + /// Gets or sets the schema name. + /// + public string SchemaName { get; set; } + + /// + /// Gets or sets the table name. + /// + public string TableName { get; set; } + +} \ No newline at end of file From 9ec87691af059a2bf41850b1515ff2e66a6f6919 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Tue, 26 Aug 2025 17:09:12 +0200 Subject: [PATCH 31/55] Partial Index tests --- .../Dialects/PostgreSQLDialectTests.cs | 2 + .../Generic/Generic_AddIndexTestsBase.cs | 147 ++++++++++++++++++ ...SQLTransformationProvider_AddIndexTests.cs | 2 +- ...verTransformationProvider_AddIndexTests.cs | 4 +- src/Migrator/Providers/Dialect.cs | 3 +- .../PostgreSQLTransformationProvider.cs | 84 +++++----- .../SQLite/SQLiteTransformationProvider.cs | 47 ++++-- .../SqlServerTransformationProvider.cs | 1 + .../Models/Indexes/Enums/FilterType.cs | 7 +- 9 files changed, 240 insertions(+), 57 deletions(-) diff --git a/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs b/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs index b49e324a..11951b71 100644 --- a/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs +++ b/src/Migrator.Tests/Dialects/PostgreSQLDialectTests.cs @@ -22,6 +22,7 @@ public void SetUp() [TestCase(FilterType.SmallerThanOrEqualTo, "<=")] [TestCase(FilterType.SmallerThan, "<")] [TestCase(FilterType.GreaterThan, ">")] + [TestCase(FilterType.NotEqualTo, "<>")] public void GetComparisonStringByFilterType_Success(FilterType filterType, string expectedString) { var result = _postgreSQLDialect.GetComparisonStringByFilterType(filterType); @@ -34,6 +35,7 @@ public void GetComparisonStringByFilterType_Success(FilterType filterType, strin [TestCase("<=", FilterType.SmallerThanOrEqualTo)] [TestCase("<", FilterType.SmallerThan)] [TestCase(">", FilterType.GreaterThan)] + [TestCase("<>", FilterType.NotEqualTo)] public void GetFilterTypeByComparisonString_Success(string comparisonString, FilterType expectedFilterType) { var result = _postgreSQLDialect.GetFilterTypeByComparisonString(comparisonString); diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index 777b11b1..2ff8a6d3 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -1,5 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; using System.Linq; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Base; using NUnit.Framework; using Index = DotNetProjects.Migrator.Framework.Index; @@ -59,4 +65,145 @@ public void AddIndex_UsingNonIndexInstanceOverload_NonUnique_ShouldBeReadable() Assert.That(index.Name, Is.EqualTo(indexName).IgnoreCase); Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); } + + [Test] + public void AddIndex_FilteredIndexSingle_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName1], + Unique = true, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } + + [Test] + public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string columnName4 = "TestColumn4"; + const string columnName5 = "TestColumn5"; + const string columnName6 = "TestColumn6"; + const string columnName7 = "TestColumn7"; + const string columnName8 = "TestColumn8"; + const string columnName9 = "TestColumn9"; + const string columnName10 = "TestColumn10"; + const string columnName11 = "TestColumn11"; + const string columnName12 = "TestColumn12"; + const string columnName13 = "TestColumn13"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16), + new Column(columnName2, DbType.Int32), + new Column(columnName3, DbType.Int64), + new Column(columnName4, DbType.UInt16), + new Column(columnName5, DbType.UInt32), + new Column(columnName6, DbType.UInt64), + new Column(columnName7, DbType.String), + new Column(columnName8, DbType.Int32), + new Column(columnName9, DbType.Int32), + new Column(columnName10, DbType.Int32), + new Column(columnName11, DbType.Int32), + new Column(columnName12, DbType.Int32), + new Column(columnName13, DbType.Int32) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 2 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName3, Value = 2323 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName4, Value = 3434 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName5, Value = -3434 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName6, Value = 3434345345 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName7, Value = "asdf" }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName8, Value = 11 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName9, Value = 22 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName10, Value = 33 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName11, Value = 44 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName12, Value = 55 }, + new() { Filter = FilterType.SmallerThanOrEqualTo, ColumnName = columnName13, Value = 66 } + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [ + columnName1, + columnName2, + columnName3, + columnName4, + columnName5, + columnName6, + columnName7, + columnName8, + columnName9, + columnName10, + columnName11, + columnName12 + ], + Unique = true, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index 7f4dc32a..66bbce3b 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using System.Data; using System.Linq; using System.Threading.Tasks; @@ -7,6 +6,7 @@ using Migrator.Tests.Providers.Generic; using Npgsql; using NUnit.Framework; +using Index = DotNetProjects.Migrator.Framework.Index; namespace Migrator.Tests.Providers.PostgreSQL; diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index 73e4cb78..20d9a57a 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -1,7 +1,9 @@ +using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; @@ -48,7 +50,7 @@ public void AddIndex_Unique_Success() } [Test] - public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() + public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_PartialIndexThrowsOnConditionMet() { // Arrange const string tableName = "TestTable"; diff --git a/src/Migrator/Providers/Dialect.cs b/src/Migrator/Providers/Dialect.cs index bd83f5e4..fcf12c33 100644 --- a/src/Migrator/Providers/Dialect.cs +++ b/src/Migrator/Providers/Dialect.cs @@ -24,7 +24,8 @@ public abstract class Dialect : IDialect new() { FilterType = FilterType.GreaterThan, FilterString = ">" }, new() { FilterType = FilterType.GreaterThanOrEqualTo, FilterString = ">=" }, new() { FilterType = FilterType.SmallerThan, FilterString = "<" }, - new() { FilterType = FilterType.SmallerThanOrEqualTo, FilterString = "<=" } + new() { FilterType = FilterType.SmallerThanOrEqualTo, FilterString = "<=" }, + new() { FilterType = FilterType.NotEqualTo, FilterString = "<>"} ]; protected Dialect() diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index 92093c91..539f1fc1 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -123,7 +123,6 @@ public override void AddIndex(string table, Index index) ExecuteNonQuery(sql); } - public override Index[] GetIndexes(string table) { var columns = GetColumns(table); @@ -131,7 +130,7 @@ public override Index[] GetIndexes(string table) // Since the migrator does not support schemas at this point in time we set the schema to "public" var schemaName = "public"; - var retVal = new List(); + var indexes = new List(); var sql = @$" SELECT @@ -172,42 +171,41 @@ FROM pg_index idx using (var cmd = CreateCommand()) using (var reader = ExecuteQuery(cmd, string.Format(sql, table))) { - var schemaNameOrdinal = reader.GetOrdinal("schema_name"); - var tableNameOrdinal = reader.GetOrdinal("table_name"); + var includeColumnsOrdinal = reader.GetOrdinal("include_columns"); + var indexColumnsOrdinal = reader.GetOrdinal("index_columns"); + var indexDefinitionOrdinal = reader.GetOrdinal("index_definition"); var indexNameOrdinal = reader.GetOrdinal("index_name"); - var isUniqueOrdinal = reader.GetOrdinal("is_unique"); var isClusteredOrdinal = reader.GetOrdinal("is_clustered"); - var isUniqueConstraintOrdinal = reader.GetOrdinal("is_unique_constraint"); var isPrimaryConstraintOrdinal = reader.GetOrdinal("is_primary_constraint"); - var indexDefinitionOrdinal = reader.GetOrdinal("index_definition"); - var indexColumnsOrdinal = reader.GetOrdinal("index_columns"); - var includeColumnsOrdinal = reader.GetOrdinal("include_columns"); + var isUniqueConstraintOrdinal = reader.GetOrdinal("is_unique_constraint"); + var isUniqueOrdinal = reader.GetOrdinal("is_unique"); var partialFilterOrdinal = reader.GetOrdinal("partial_filter"); + var schemaNameOrdinal = reader.GetOrdinal("schema_name"); + var tableNameOrdinal = reader.GetOrdinal("table_name"); while (reader.Read()) { if (!reader.IsDBNull(1)) { - var indexDefinition = reader.GetString(indexDefinitionOrdinal); + var includeColumns = !reader.IsDBNull(includeColumnsOrdinal) ? reader.GetString(includeColumnsOrdinal) : null; var indexColumns = !reader.IsDBNull(indexColumnsOrdinal) ? reader.GetString(indexColumnsOrdinal) : null; + var indexDefinition = reader.GetString(indexDefinitionOrdinal); var partialColumns = !reader.IsDBNull(partialFilterOrdinal) ? reader.GetString(partialFilterOrdinal) : null; - var includeColumns = !reader.IsDBNull(includeColumnsOrdinal) ? reader.GetString(includeColumnsOrdinal) : null; + List filterItems = []; if (!string.IsNullOrWhiteSpace(partialColumns)) { partialColumns = partialColumns.Substring(1, partialColumns.Length - 2); - var partialSplitted = Regex.Split(partialColumns, " AND ").Select(x => x.Trim()).Select(x => x.Substring(1, x.Length - 2)).ToList(); - var comparisonStrings = _dialect.GetComparisonStrings(); - - List filterItems = []; + var andSplitted = Regex.Split(partialColumns, " AND ").Select(x => x.Trim()).ToList(); + var partialSplitted = andSplitted.Select(x => x.Substring(1, x.Length - 2)).ToList(); foreach (var partialItemString in partialSplitted) { string[] splits = []; var filterType = FilterType.None; - foreach (var comparisonString in comparisonStrings) + foreach (var comparisonString in comparisonStrings.OrderByDescending(x => x)) { splits = Regex.Split(partialItemString, $" {comparisonString} "); @@ -220,24 +218,29 @@ FROM pg_index idx if (splits.Length != 2) { - throw new NotImplementedException($"Comparison string in '{partialItemString}'"); + throw new NotImplementedException($"Comparison string not found in '{partialItemString}'"); } var columnNameString = splits[0]; - var columnNameRegex = new Regex(@"(?<=^\().+(?=\)::(text|boolean|integer)$)"); + if (columnNameRegex.Match(columnNameString) is Match matchColumnName && matchColumnName.Success) { columnNameString = matchColumnName.Value; } var column = columns.First(x => columnNameString.Equals(x.Name, StringComparison.OrdinalIgnoreCase)); + var valueAsString = splits[1]; + var stringValueNumericRegex = new Regex(@"(?<=^\()[^\)]+(?=\)::numeric$)"); - var stringValueRegex = new Regex("(?<=^').+(?='::(text|boolean|integer)$)"); + if (stringValueNumericRegex.Match(valueAsString) is Match valueNumericMatch && valueNumericMatch.Success) + { + valueAsString = valueNumericMatch.Value; + } - var valueAsString = splits[1]; + var stringValueRegex = new Regex("(?<=^').+(?='::(text|boolean|integer|bigint)$)"); - if (stringValueRegex.Match(splits[1]) is Match match && match.Success) + if (stringValueRegex.Match(valueAsString) is Match match && match.Success) { valueAsString = match.Value; } @@ -246,42 +249,43 @@ FROM pg_index idx { ColumnName = column.Name, Filter = filterType, - }; - - filterItem.Value = column.MigratorDbType switch - { - MigratorDbType.Int16 => short.Parse(valueAsString), - MigratorDbType.Int32 => int.Parse(valueAsString), - MigratorDbType.Int64 => long.Parse(valueAsString), - MigratorDbType.UInt16 => ushort.Parse(valueAsString), - MigratorDbType.UInt32 => uint.Parse(valueAsString), - MigratorDbType.UInt64 => ulong.Parse(valueAsString), - MigratorDbType.Boolean => valueAsString == "1" || valueAsString.Equals("true", StringComparison.OrdinalIgnoreCase), - MigratorDbType.String => valueAsString, - _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + Value = column.MigratorDbType switch + { + MigratorDbType.Int16 => short.Parse(valueAsString), + MigratorDbType.Int32 => int.Parse(valueAsString), + MigratorDbType.Int64 => long.Parse(valueAsString), + MigratorDbType.UInt16 => ushort.Parse(valueAsString), + MigratorDbType.UInt32 => uint.Parse(valueAsString), + MigratorDbType.UInt64 => ulong.Parse(valueAsString), + MigratorDbType.Decimal => decimal.Parse(valueAsString), + MigratorDbType.Boolean => valueAsString == "1" || valueAsString.Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => valueAsString, + _ => throw new NotImplementedException($"Type '{column.MigratorDbType}' not yet supported - there are many variations. Please file an issue."), + } }; filterItems.Add(filterItem); } } - var idx = new Index + var index = new Index { + Clustered = !reader.IsDBNull(isClusteredOrdinal) && reader.GetBoolean(isClusteredOrdinal), + FilterItems = filterItems, + IncludeColumns = !string.IsNullOrWhiteSpace(includeColumns) ? [.. includeColumns.Split(',').Select(x => x.Trim())] : null, + KeyColumns = !string.IsNullOrWhiteSpace(indexColumns) ? [.. indexColumns.Split(',').Select(x => x.Trim())] : null, Name = reader.GetString(indexNameOrdinal), PrimaryKey = !reader.IsDBNull(isPrimaryConstraintOrdinal) && reader.GetBoolean(isPrimaryConstraintOrdinal), Unique = !reader.IsDBNull(isUniqueOrdinal) && reader.GetBoolean(isUniqueOrdinal), UniqueConstraint = !reader.IsDBNull(isUniqueConstraintOrdinal) && reader.GetBoolean(isUniqueConstraintOrdinal), - Clustered = !reader.IsDBNull(isClusteredOrdinal) && reader.GetBoolean(isClusteredOrdinal), - IncludeColumns = !string.IsNullOrWhiteSpace(includeColumns) ? [.. includeColumns.Split(',').Select(x => x.Trim())] : null, - KeyColumns = !string.IsNullOrWhiteSpace(indexColumns) ? [.. indexColumns.Split(',').Select(x => x.Trim())] : null, }; - retVal.Add(idx); + indexes.Add(index); } } } - return retVal.ToArray(); + return [.. indexes]; } public override void RemoveTable(string name) diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index 6da22847..fb02e0a4 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1260,19 +1260,40 @@ public override Index[] GetIndexes(string table) var column = columns.Single(x => x.Name.Equals(splitted[0], StringComparison.OrdinalIgnoreCase)); - filterItem.Value = column.MigratorDbType switch - { - MigratorDbType.Int16 => short.Parse(splitted[2]), - MigratorDbType.Int32 => int.Parse(splitted[2]), - MigratorDbType.Int64 => long.Parse(splitted[2]), - MigratorDbType.UInt16 => ushort.Parse(splitted[2]), - MigratorDbType.UInt32 => uint.Parse(splitted[2]), - MigratorDbType.UInt64 => ulong.Parse(splitted[2]), - MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), - MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), - _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + var sqliteIntegerDataTypes = new[] { + MigratorDbType.Int16, + MigratorDbType.Int32, + MigratorDbType.Int64, + MigratorDbType.UInt16, + MigratorDbType.UInt32, + MigratorDbType.UInt64 }; + if (sqliteIntegerDataTypes.Contains(column.MigratorDbType)) + { + if (long.TryParse(splitted[2], out var longValue)) + { + filterItem.Value = longValue; + } + else if (ulong.TryParse(splitted[2], out var uLongValue)) + { + filterItem.Value = uLongValue; + } + else + { + throw new Exception(); + } + } + else + { + filterItem.Value = column.MigratorDbType switch + { + MigratorDbType.Boolean => splitted[2] == "1" || splitted[2].Equals("true", StringComparison.OrdinalIgnoreCase), + MigratorDbType.String => splitted[2].Substring(1, splitted[2].Length - 2), + _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), + }; + } + index.FilterItems.Add(filterItem); } } @@ -1439,9 +1460,9 @@ public override void AddIndex(string table, Index index) _ => throw new NotImplementedException("Given type is not implemented. Please file an issue."), }; - if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo) + if ((filterItem.Value is string || filterItem.Value is bool) && filterItem.Filter != FilterType.EqualTo && filterItem.Filter != FilterType.NotEqualTo) { - throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with {nameof(FilterType.EqualTo)}."); + throw new MigrationException($"Bool and string in {nameof(FilterItem)} can only be used with '{nameof(FilterType.EqualTo)}' or '{nameof(FilterType.EqualTo)}'."); } var singleFilterString = $"{filterColumnQuoted} {comparisonString} {value}"; diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index fa0953e2..85eb246f 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -420,6 +420,7 @@ ORDER BY MigratorDbType.UInt16 => ushort.Parse(valueAsString), MigratorDbType.UInt32 => uint.Parse(valueAsString), MigratorDbType.UInt64 => ulong.Parse(valueAsString), + MigratorDbType.Decimal => decimal.Parse(valueAsString), MigratorDbType.Boolean => valueAsString == "1" || valueAsString.Equals("true", StringComparison.OrdinalIgnoreCase), MigratorDbType.String => valueAsString, _ => throw new NotImplementedException("Type not yet supported. Please file an issue."), diff --git a/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs b/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs index cc5706ac..44ad2b5a 100644 --- a/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs +++ b/src/Migrator/Providers/Models/Indexes/Enums/FilterType.cs @@ -27,5 +27,10 @@ public enum FilterType /// /// Smaller than or equal to /// - SmallerThanOrEqualTo + SmallerThanOrEqualTo, + + /// + /// Not equal to + /// + NotEqualTo } \ No newline at end of file From 515829551c06df6194284d318a7c47e9c1ad2d25 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Tue, 26 Aug 2025 17:27:16 +0200 Subject: [PATCH 32/55] Added include columns tests for SQL Server --- ...verTransformationProvider_AddIndexTests.cs | 62 ++++++++++++++++++- .../SqlServerTransformationProvider.cs | 5 +- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index 20d9a57a..f715cf13 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -1,9 +1,7 @@ -using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; -using DotNetProjects.Migrator.Providers.Models.Indexes; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; @@ -85,4 +83,64 @@ public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_PartialIndexThrowsOnC Assert.That(index.Unique, Is.True); Assert.That(sqlException.Number, Is.EqualTo(2601)); } + + [Test] + public void AddIndex_IncludeColumnsSingle_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + IncludeColumns = [columnName2] + }); + + // Assert + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(index.KeyColumns.Single, Is.EqualTo(columnName).IgnoreCase); + Assert.That(index.IncludeColumns.Single, Is.EqualTo(columnName2).IgnoreCase); + } + + [Test] + public void AddIndex_IncludeColumnsMultiple_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32), new Column(columnName2, DbType.String), new Column(columnName3, DbType.Boolean)); + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName], + Unique = true, + IncludeColumns = [columnName2, columnName3] + }); + + // Assert + var index = Provider.GetIndexes(tableName).Single(); + + Assert.That(index.Unique, Is.True); + Assert.That(index.KeyColumns.Single, Is.EqualTo(columnName).IgnoreCase); + Assert.That(index.IncludeColumns, Is.EquivalentTo([columnName2, columnName3]) + .Using((x, y) => string.Compare(x, y, ignoreCase: true))); + } } diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 85eb246f..739e0f69 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -149,6 +149,7 @@ public override void AddIndex(string table, Index index) var uniqueString = index.Unique ? "UNIQUE" : null; var columnsString = $"({string.Join(", ", columns)})"; + var includeString = hasIncludedColumns ? $"INCLUDE ({string.Join(", ", index.IncludeColumns)})" : null; var filterString = string.Empty; var clusteredString = index.Clustered ? "CLUSTERED" : "NONCLUSTERED"; @@ -201,6 +202,7 @@ public override void AddIndex(string table, Index index) list.Add("ON"); list.Add(table); list.Add(columnsString); + list.Add(includeString); list.Add(filterString); list = [.. list.Where(x => !string.IsNullOrWhiteSpace(x))]; @@ -334,7 +336,8 @@ ORDER BY { Clustered = reader.GetString(indexTypeOrdinal) == "CLUSTERED", ColumnName = reader.GetString(columnNameOrdinal), - FilterString = reader.GetString(filterDefinitionOrdinal), + ColumnOrder = reader.GetInt32(columnOrderOrdinal), + FilterString = !reader.IsDBNull(filterDefinitionOrdinal) ? reader.GetString(filterDefinitionOrdinal) : null, IsFilteredIndex = reader.GetBoolean(isFilteredIndexOrdinal), IsIncludedColumn = reader.GetBoolean(isIncludedColumnOrdinal), Name = reader.GetString(indexNameOrdinal), From c555953a249f0f38601bdfe5ecdc9beb184c0413 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 12:21:29 +0200 Subject: [PATCH 33/55] Update --- .../Generic/Generic_AddIndexTestsBase.cs | 141 ---------------- .../Generic/Generic_GetIndexesTestsBase.cs | 6 +- ...SQLTransformationProvider_AddIndexTests.cs | 152 +++++++++++++++++ ...verTransformationProvider_AddIndexTests.cs | 153 ++++++++++++++++++ ...iteTransformationProvider_AddIndexTests.cs | 153 ++++++++++++++++++ ...iteTransformationProvider_AddTableTests.cs | 5 +- .../Framework/ITransformationProvider.cs | 8 +- src/Migrator/Framework/Index.cs | 1 + .../Oracle/OracleTransformationProvider.cs | 134 +++++++++++---- .../PostgreSQLTransformationProvider.cs | 8 +- .../Providers/Models/Indexes/IndexItem.cs | 5 - 11 files changed, 576 insertions(+), 190 deletions(-) diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index 2ff8a6d3..4da3d806 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -65,145 +65,4 @@ public void AddIndex_UsingNonIndexInstanceOverload_NonUnique_ShouldBeReadable() Assert.That(index.Name, Is.EqualTo(indexName).IgnoreCase); Assert.That(index.KeyColumns.Single(), Is.EqualTo(columnName).IgnoreCase); } - - [Test] - public void AddIndex_FilteredIndexSingle_Success() - { - // Arrange - const string tableName = "TestTable"; - const string columnName1 = "TestColumn1"; - - const string indexName = "TestIndexName"; - - Provider.AddTable(tableName, - new Column(columnName1, DbType.Int16) - ); - - List filterItems = [ - new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, - ]; - - // Act - Provider.AddIndex(tableName, - new Index - { - Name = indexName, - KeyColumns = [columnName1], - Unique = true, - FilterItems = filterItems - }); - - // Assert - - var indexesFromDatabase = Provider.GetIndexes(table: tableName); - var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; - - // We cannot find out the exact DbType so we compare strings. - foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) - { - var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); - Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); - Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); - } - - Assert.That( - filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), - Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) - ); - } - - [Test] - public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() - { - // Arrange - const string tableName = "TestTable"; - const string columnName1 = "TestColumn1"; - const string columnName2 = "TestColumn2"; - const string columnName3 = "TestColumn3"; - const string columnName4 = "TestColumn4"; - const string columnName5 = "TestColumn5"; - const string columnName6 = "TestColumn6"; - const string columnName7 = "TestColumn7"; - const string columnName8 = "TestColumn8"; - const string columnName9 = "TestColumn9"; - const string columnName10 = "TestColumn10"; - const string columnName11 = "TestColumn11"; - const string columnName12 = "TestColumn12"; - const string columnName13 = "TestColumn13"; - - const string indexName = "TestIndexName"; - - Provider.AddTable(tableName, - new Column(columnName1, DbType.Int16), - new Column(columnName2, DbType.Int32), - new Column(columnName3, DbType.Int64), - new Column(columnName4, DbType.UInt16), - new Column(columnName5, DbType.UInt32), - new Column(columnName6, DbType.UInt64), - new Column(columnName7, DbType.String), - new Column(columnName8, DbType.Int32), - new Column(columnName9, DbType.Int32), - new Column(columnName10, DbType.Int32), - new Column(columnName11, DbType.Int32), - new Column(columnName12, DbType.Int32), - new Column(columnName13, DbType.Int32) - ); - - List filterItems = [ - new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, - new() { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 2 }, - new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName3, Value = 2323 }, - new() { Filter = FilterType.NotEqualTo, ColumnName = columnName4, Value = 3434 }, - new() { Filter = FilterType.NotEqualTo, ColumnName = columnName5, Value = -3434 }, - new() { Filter = FilterType.SmallerThan, ColumnName = columnName6, Value = 3434345345 }, - new() { Filter = FilterType.NotEqualTo, ColumnName = columnName7, Value = "asdf" }, - new() { Filter = FilterType.EqualTo, ColumnName = columnName8, Value = 11 }, - new() { Filter = FilterType.GreaterThan, ColumnName = columnName9, Value = 22 }, - new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName10, Value = 33 }, - new() { Filter = FilterType.NotEqualTo, ColumnName = columnName11, Value = 44 }, - new() { Filter = FilterType.SmallerThan, ColumnName = columnName12, Value = 55 }, - new() { Filter = FilterType.SmallerThanOrEqualTo, ColumnName = columnName13, Value = 66 } - ]; - - // Act - Provider.AddIndex(tableName, - new Index - { - Name = indexName, - KeyColumns = [ - columnName1, - columnName2, - columnName3, - columnName4, - columnName5, - columnName6, - columnName7, - columnName8, - columnName9, - columnName10, - columnName11, - columnName12 - ], - Unique = true, - FilterItems = filterItems - }); - - // Assert - - var indexesFromDatabase = Provider.GetIndexes(table: tableName); - var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; - - // We cannot find out the exact DbType so we compare strings. - foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) - { - var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); - Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); - Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); - } - - Assert.That( - filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), - Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) - ); - } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs index eefd0764..ffceaf9d 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_GetIndexesTestsBase.cs @@ -47,9 +47,9 @@ public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() var filterItem2 = index.FilterItems.Single(x => x.ColumnName == columnName2); Assert.That(filterItem1.Filter, Is.EqualTo(FilterType.GreaterThanOrEqualTo)); - Assert.That((int)filterItem1.Value, Is.EqualTo(100)); + Assert.That((long)filterItem1.Value, Is.EqualTo(100)); - Assert.That(filterItem1.Filter, Is.EqualTo(FilterType.EqualTo)); - Assert.That((string)filterItem1.Value, Is.EqualTo("Hello")); + Assert.That(filterItem2.Filter, Is.EqualTo(FilterType.EqualTo)); + Assert.That((string)filterItem2.Value, Is.EqualTo("Hello")); } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index 66bbce3b..5ef8ad29 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -1,7 +1,11 @@ +using System; +using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using Npgsql; @@ -160,4 +164,152 @@ public void AddIndex_IncludeColumnsMultiple_Success() Assert.That(index.IncludeColumns, Is.EquivalentTo([columnName2, columnName3]) .Using((x, y) => string.Compare(x, y, ignoreCase: true))); } + + /// + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// + [Test] + public void AddIndex_FilteredIndexSingle_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName1], + Unique = true, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } + + /// + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// + [Test] + public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string columnName4 = "TestColumn4"; + const string columnName5 = "TestColumn5"; + const string columnName6 = "TestColumn6"; + const string columnName7 = "TestColumn7"; + const string columnName8 = "TestColumn8"; + const string columnName9 = "TestColumn9"; + const string columnName10 = "TestColumn10"; + const string columnName11 = "TestColumn11"; + const string columnName12 = "TestColumn12"; + const string columnName13 = "TestColumn13"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16), + new Column(columnName2, DbType.Int32), + new Column(columnName3, DbType.Int64), + new Column(columnName4, DbType.UInt16), + new Column(columnName5, DbType.UInt32), + new Column(columnName6, DbType.UInt64), + new Column(columnName7, DbType.String), + new Column(columnName8, DbType.Int32), + new Column(columnName9, DbType.Int32), + new Column(columnName10, DbType.Int32), + new Column(columnName11, DbType.Int32), + new Column(columnName12, DbType.Int32), + new Column(columnName13, DbType.Int32) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 2 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName3, Value = 2323 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName4, Value = 3434 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName5, Value = -3434 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName6, Value = 3434345345 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName7, Value = "asdf" }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName8, Value = 11 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName9, Value = 22 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName10, Value = 33 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName11, Value = 44 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName12, Value = 55 }, + new() { Filter = FilterType.SmallerThanOrEqualTo, ColumnName = columnName13, Value = 66 } + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [ + columnName1, + columnName2, + columnName3, + columnName4, + columnName5, + columnName6, + columnName7, + columnName8, + columnName9, + columnName10, + columnName11, + columnName12, + columnName13 + ], + Unique = false, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } } diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index f715cf13..a9f52f30 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -1,10 +1,15 @@ +using System; +using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; +using Index = DotNetProjects.Migrator.Framework.Index; namespace Migrator.Tests.Providers.SQLServer; @@ -143,4 +148,152 @@ public void AddIndex_IncludeColumnsMultiple_Success() Assert.That(index.IncludeColumns, Is.EquivalentTo([columnName2, columnName3]) .Using((x, y) => string.Compare(x, y, ignoreCase: true))); } + + /// + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// + [Test] + public void AddIndex_FilteredIndexSingle_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName1], + Unique = true, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } + + /// + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// + [Test] + public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string columnName4 = "TestColumn4"; + const string columnName5 = "TestColumn5"; + const string columnName6 = "TestColumn6"; + const string columnName7 = "TestColumn7"; + const string columnName8 = "TestColumn8"; + const string columnName9 = "TestColumn9"; + const string columnName10 = "TestColumn10"; + const string columnName11 = "TestColumn11"; + const string columnName12 = "TestColumn12"; + const string columnName13 = "TestColumn13"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16), + new Column(columnName2, DbType.Int32), + new Column(columnName3, DbType.Int64), + new Column(columnName4, DbType.UInt16), + new Column(columnName5, DbType.UInt32), + new Column(columnName6, DbType.UInt64), + new Column(columnName7, DbType.String), + new Column(columnName8, DbType.Int32), + new Column(columnName9, DbType.Int32), + new Column(columnName10, DbType.Int32), + new Column(columnName11, DbType.Int32), + new Column(columnName12, DbType.Int32), + new Column(columnName13, DbType.Int32) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 2 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName3, Value = 2323 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName4, Value = 3434 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName5, Value = -3434 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName6, Value = 3434345345 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName7, Value = "asdf" }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName8, Value = 11 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName9, Value = 22 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName10, Value = 33 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName11, Value = 44 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName12, Value = 55 }, + new() { Filter = FilterType.SmallerThanOrEqualTo, ColumnName = columnName13, Value = 66 } + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [ + columnName1, + columnName2, + columnName3, + columnName4, + columnName5, + columnName6, + columnName7, + columnName8, + columnName9, + columnName10, + columnName11, + columnName12, + columnName13 + ], + Unique = false, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs index f81f49df..cf146f4c 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -1,11 +1,16 @@ +using System; +using System.Collections.Generic; using System.Data; using System.Data.SQLite; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; +using Index = DotNetProjects.Migrator.Framework.Index; namespace Migrator.Tests.Providers.SQLite; @@ -103,6 +108,154 @@ public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() Assert.That(indexScriptFromDatabase, Is.EqualTo("CREATE UNIQUE INDEX TestIndexName ON TestTable (TestColumn, TestColumn2, TestColumn3) WHERE TestColumn >= 100 AND TestColumn2 = 'Hello' AND TestColumn3 = 1")); } + /// + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// + [Test] + public void AddIndex_FilteredIndexSingle_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName1], + Unique = true, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } + + /// + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// + [Test] + public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string columnName4 = "TestColumn4"; + const string columnName5 = "TestColumn5"; + const string columnName6 = "TestColumn6"; + const string columnName7 = "TestColumn7"; + const string columnName8 = "TestColumn8"; + const string columnName9 = "TestColumn9"; + const string columnName10 = "TestColumn10"; + const string columnName11 = "TestColumn11"; + const string columnName12 = "TestColumn12"; + const string columnName13 = "TestColumn13"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16), + new Column(columnName2, DbType.Int32), + new Column(columnName3, DbType.Int64), + new Column(columnName4, DbType.UInt16), + new Column(columnName5, DbType.UInt32), + new Column(columnName6, DbType.UInt64), + new Column(columnName7, DbType.String), + new Column(columnName8, DbType.Int32), + new Column(columnName9, DbType.Int32), + new Column(columnName10, DbType.Int32), + new Column(columnName11, DbType.Int32), + new Column(columnName12, DbType.Int32), + new Column(columnName13, DbType.Int32) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 2 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName3, Value = 2323 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName4, Value = 3434 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName5, Value = -3434 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName6, Value = 3434345345 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName7, Value = "asdf" }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName8, Value = 11 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName9, Value = 22 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName10, Value = 33 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName11, Value = 44 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName12, Value = 55 }, + new() { Filter = FilterType.SmallerThanOrEqualTo, ColumnName = columnName13, Value = 66 } + ]; + + // Act + Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [ + columnName1, + columnName2, + columnName3, + columnName4, + columnName5, + columnName6, + columnName7, + columnName8, + columnName9, + columnName10, + columnName11, + columnName12, + columnName13 + ], + Unique = false, + FilterItems = filterItems + }); + + // Assert + + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; + + // We cannot find out the exact DbType so we compare strings. + foreach (var filteredItemFromDatabase in filteredItemsFromDatabase) + { + var expected = filterItems.Single(x => x.ColumnName.Equals(filteredItemFromDatabase.ColumnName, StringComparison.OrdinalIgnoreCase)); + Assert.That(filteredItemFromDatabase.Filter, Is.EqualTo(expected.Filter)); + Assert.That(Convert.ToString(filteredItemFromDatabase.Value, CultureInfo.InvariantCulture), Is.EqualTo(Convert.ToString(expected.Value, CultureInfo.InvariantCulture))); + } + + Assert.That( + filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), + Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) + ); + } + private string GetCreateIndexSqlString(string indexName) { using var cmd = Provider.CreateCommand(); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs index af0fb356..7d00f492 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddTableTests.cs @@ -3,6 +3,7 @@ using System.Linq; using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.SQLite; +using Microsoft.Data.Sqlite; using Migrator.Tests.Providers.SQLite.Base; using NUnit.Framework; @@ -77,7 +78,7 @@ public void AddTable_SinglePrimaryKey_ContainsNull() ); Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); - Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 2])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 2])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); @@ -105,7 +106,7 @@ public void AddTable_MiscellaneousColumns_Succeeds() ); Provider.Insert(tableName, [columnName1, columnName2], [1, 1]); - Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); + Assert.Throws(() => Provider.Insert(tableName, [columnName1, columnName2], [1, 1])); // Assert var createScript = ((SQLiteTransformationProvider)Provider).GetSqlCreateTableScript(tableName); diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index 7d01117a..969dd091 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -393,10 +393,12 @@ public interface ITransformationProvider : IDisposable Index[] GetIndexes(string table); /// - /// Get the information about the columns in a table + /// Get the information about the columns in a table. + /// and can in some cases only be guessed. Do not rely on them. Same for /// /// The table name that you want the columns for. /// + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is nust a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] Column[] GetColumns(string table); /// @@ -408,11 +410,13 @@ public interface ITransformationProvider : IDisposable int GetColumnContentSize(string table, string columnName); /// - /// Get information about a single column in a table + /// Get information about a single column in a table. + /// and can in some cases only be guessed. Do not rely on them. Same for /// /// The table name that you want the columns for. /// The column name for which you want information. /// + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is nust a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] Column GetColumnByName(string table, string column); /// diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index 5376a944..ac8c9f29 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -39,6 +39,7 @@ public class Index : IDbField /// /// Gets or sets items that represent filter expressions in filtered indexes. Currently string, integer and boolean values are supported. /// Attention: In SQL Server the column used in the filter must be NOT NULL. + /// Oracle: Not supported for Oracle /// public List FilterItems { get; set; } = []; } diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index a98391e2..bf9eaaeb 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -1,6 +1,7 @@ using DotNetProjects.Migrator.Framework; using DotNetProjects.Migrator.Providers.Impl.Oracle.Models; using DotNetProjects.Migrator.Providers.Models; +using DotNetProjects.Migrator.Providers.Models.Indexes; using System; using System.Collections.Generic; using System.Data; @@ -135,6 +136,41 @@ public override void AddForeignKey(string name, string primaryTable, string[] pr ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4})", primaryTable, name, primaryColumnsSql, refTable, refColumnsSql)); } + public override void AddIndex(string table, Index index) + { + ValidateIndex(tableName: table, index: index); + + var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; + // Included columns and Clustered indexes are not supported in Oracle. We ignore the values given in the properties silently. + + var name = QuoteConstraintNameIfRequired(index.Name); + table = QuoteTableNameIfRequired(table); + var columns = QuoteColumnNamesIfRequired(index.KeyColumns); + + var uniqueString = index.Unique ? "UNIQUE" : null; + var columnsString = $"({string.Join(", ", columns)})"; + + if (index.FilterItems != null && index.FilterItems.Count > 0) + { + throw new NotSupportedException($"Oracle: This migrator does not support partial indexes for Oracle at this point in time. Please use 'if(Provider is {nameof(OracleTransformationProvider)})'"); + } + + List list = []; + list.Add("CREATE"); + list.Add(uniqueString); + list.Add("INDEX"); + list.Add(name); + list.Add("ON"); + list.Add(table); + list.Add(columnsString); + + list = [.. list.Where(x => !string.IsNullOrWhiteSpace(x))]; + + var sql = string.Join(" ", list); + + ExecuteNonQuery(sql); + } + private void GuardAgainstMaximumIdentifierLengthForOracle(string name) { var utf8Bytes = Encoding.UTF8.GetBytes(name); @@ -839,55 +875,83 @@ private string SchemaInfoTableName public override Index[] GetIndexes(string table) { - var sql = "select user_indexes.index_name, constraint_type, uniqueness " + - "from user_indexes left outer join user_constraints on user_indexes.index_name = user_constraints.constraint_name " + - "where lower(user_indexes.table_name) = lower('{0}') and index_type = 'NORMAL'"; - - sql = string.Format(sql, table); - - var indexes = new List(); + var sql = @$"SELECT + i.table_name, + i.index_name, + i.uniqueness, + ic.column_position, + ic.column_name, + CASE WHEN c.constraint_type = 'P' THEN 'YES' ELSE 'NO' END AS is_primary_key, + CASE WHEN c.constraint_type = 'U' THEN 'YES' ELSE 'NO' END AS is_unique_key + FROM + user_indexes i + JOIN + user_ind_columns ic ON i.index_name = ic.index_name AND + i.table_name = ic.table_name + LEFT JOIN + user_constraints c ON i.index_name = c.index_name AND + i.table_name = c.table_name + WHERE + UPPER(i.table_name) = '{table.ToUpperInvariant()}' AND + i.index_type = 'NORMAL' + ORDER BY + i.table_name, i.index_name, ic.column_position"; + + List indexItems = []; using (var cmd = CreateCommand()) using (var reader = ExecuteQuery(cmd, sql)) { while (reader.Read()) { - var index = new Index + var tableNameOrdinal = reader.GetOrdinal("table_name"); + var indexNameOrdinal = reader.GetOrdinal("index_name"); + var uniquenessOrdinal = reader.GetOrdinal("uniqueness"); + var columnPositionOrdinal = reader.GetOrdinal("column_position"); + var columnNameOrdinal = reader.GetOrdinal("column_name"); + var isPrimaryKeyOrdinal = reader.GetOrdinal("is_primary_key"); + var isUniqueConstraintOrdinal = reader.GetOrdinal("is_unique_key"); + + var indexItem = new IndexItem { - Name = reader.GetString(0), - Unique = reader.GetString(2) == "UNIQUE" ? true : false + ColumnName = reader.GetString(columnNameOrdinal), + ColumnOrder = reader.GetInt32(columnPositionOrdinal), + Name = reader.GetString(indexNameOrdinal), + PrimaryKey = reader.GetString(isPrimaryKeyOrdinal) == "YES", + TableName = reader.GetString(tableNameOrdinal), + Unique = reader.GetString(uniquenessOrdinal) == "UNIQUE", + UniqueConstraint = reader.GetString(isUniqueConstraintOrdinal) == "YES" }; - if (!reader.IsDBNull(1)) - { - index.PrimaryKey = reader.GetString(1) == "P" ? true : false; - index.UniqueConstraint = reader.GetString(1) == "C" ? true : false; - } - else - { - index.PrimaryKey = false; - } - - index.Clustered = false; //??? - - //if (!reader.IsDBNull(3)) index.KeyColumns = (reader.GetString(3).Split(',')); - //if (!reader.IsDBNull(4)) index.IncludeColumns = (reader.GetString(4).Split(',')); - - indexes.Add(index); + indexItems.Add(indexItem); } } - foreach (var idx in indexes) + var indexGroups = indexItems.GroupBy(x => new { x.SchemaName, x.TableName, x.Name }); + List indexes = []; + + foreach (var indexGroup in indexGroups) { - sql = "SELECT column_Name FROM all_ind_columns WHERE lower(table_name) = lower('" + table + "') and lower(index_name) = lower('" + idx.Name + "')"; - using var cmd = CreateCommand(); - using var reader = ExecuteQuery(cmd, sql); - var columns = new List(); - while (reader.Read()) + var first = indexGroup.First(); + + var index = new Index { - columns.Add(reader.GetString(0)); - } - idx.KeyColumns = columns.ToArray(); + KeyColumns = [.. indexGroup.OrderBy(x => x.ColumnOrder).Select(x => x.ColumnName).Distinct()], + Name = first.Name, + PrimaryKey = first.PrimaryKey, + UniqueConstraint = first.UniqueConstraint, + Unique = first.Unique, + + // Oracle does not support clustered indexes at this point in time. + Clustered = false, + + // Oracle does not support include columns at this point in time. + IncludeColumns = null, + }; + + // FilterItems is not supported in this migrator at this point in time. + + indexes.Add(index); } return indexes.ToArray(); diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index 539f1fc1..b2a03bf9 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -197,8 +197,12 @@ FROM pg_index idx { partialColumns = partialColumns.Substring(1, partialColumns.Length - 2); var comparisonStrings = _dialect.GetComparisonStrings(); - var andSplitted = Regex.Split(partialColumns, " AND ").Select(x => x.Trim()).ToList(); - var partialSplitted = andSplitted.Select(x => x.Substring(1, x.Length - 2)).ToList(); + var partialSplitted = Regex.Split(partialColumns, " AND ").Select(x => x.Trim()).ToList(); + + if (partialSplitted.Count > 1) + { + partialSplitted = partialSplitted.Select(x => x.Substring(1, x.Length - 2)).ToList(); + } foreach (var partialItemString in partialSplitted) { diff --git a/src/Migrator/Providers/Models/Indexes/IndexItem.cs b/src/Migrator/Providers/Models/Indexes/IndexItem.cs index a392a57f..73f64d5e 100644 --- a/src/Migrator/Providers/Models/Indexes/IndexItem.cs +++ b/src/Migrator/Providers/Models/Indexes/IndexItem.cs @@ -12,9 +12,6 @@ public class IndexItem /// public int ColumnOrder { get; set; } - - - /// /// Gets or sets the index name. /// @@ -25,8 +22,6 @@ public class IndexItem /// public bool Unique { get; set; } - - /// /// Indicates whether it is a primary key constraint. /// From 76120fdc31d8ce36c179846bf98853b975d980c3 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 15:17:21 +0200 Subject: [PATCH 34/55] Added sql assertion in tests for AddIndex --- .../Generic/Generic_AddIndexTestsBase.cs | 2 +- ...cleTransformationProvider_AddIndexTests.cs | 132 ++++++++++++++++++ ...SQLTransformationProvider_AddIndexTests.cs | 8 +- ...verTransformationProvider_AddIndexTests.cs | 4 +- ...iteTransformationProvider_AddIndexTests.cs | 8 +- .../Framework/ITransformationProvider.cs | 9 +- .../Oracle/OracleTransformationProvider.cs | 70 ++++++++-- .../PostgreSQLTransformationProvider.cs | 4 +- .../SQLite/SQLiteTransformationProvider.cs | 4 +- .../SqlServerTransformationProvider.cs | 4 +- .../Providers/NoOpTransformationProvider.cs | 10 +- .../Providers/TransformationProvider.cs | 6 +- 12 files changed, 233 insertions(+), 28 deletions(-) diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index 4da3d806..1fee4b54 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -52,7 +52,7 @@ public void AddIndex_UsingNonIndexInstanceOverload_NonUnique_ShouldBeReadable() const string columnName = "TestColumn"; const string indexName = "TestIndexName"; - Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); + Provider.AddTable(tableName, new Column(columnName, DbType.Int32)); // Act Provider.AddIndex(indexName, tableName, columnName); diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index 09d97d41..06ce4b95 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -1,10 +1,16 @@ +using System; +using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; +using DotNetProjects.Migrator.Providers.Models.Indexes; +using DotNetProjects.Migrator.Providers.Models.Indexes.Enums; using Migrator.Tests.Providers.Generic; using NUnit.Framework; using Oracle.ManagedDataAccess.Client; +using Index = DotNetProjects.Migrator.Framework.Index; namespace Migrator.Tests.Providers.OracleProvider; @@ -46,4 +52,130 @@ public void AddIndex_Unique_Success() Assert.That(index.Unique, Is.True); Assert.That(ex.Number, Is.EqualTo(1)); } + + /// + /// This test is located in the dedicated database type folder not in the base class since + /// cannot read filter items. + /// + [Test] + public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string columnName3 = "TestColumn3"; + const string columnName4 = "TestColumn4"; + const string columnName5 = "TestColumn5"; + const string columnName6 = "TestColumn6"; + const string columnName7 = "TestColumn7"; + const string columnName8 = "TestColumn8"; + const string columnName9 = "TestColumn9"; + const string columnName10 = "TestColumn10"; + const string columnName11 = "TestColumn11"; + const string columnName12 = "TestColumn12"; + const string columnName13 = "TestColumn13"; + + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16), + new Column(columnName2, DbType.Int32), + new Column(columnName3, DbType.Int64), + new Column(columnName4, DbType.UInt16), + new Column(columnName5, DbType.UInt32), + new Column(columnName6, DbType.UInt64), + new Column(columnName7, DbType.String), + new Column(columnName8, DbType.Int32), + new Column(columnName9, DbType.Int32), + new Column(columnName10, DbType.Int32), + new Column(columnName11, DbType.Int32), + new Column(columnName12, DbType.Int32), + new Column(columnName13, DbType.Int32) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 2 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName3, Value = 2323 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName4, Value = 3434 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName5, Value = -3434 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName6, Value = 3434345345 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName7, Value = "asdf" }, + new() { Filter = FilterType.EqualTo, ColumnName = columnName8, Value = 11 }, + new() { Filter = FilterType.GreaterThan, ColumnName = columnName9, Value = 22 }, + new() { Filter = FilterType.GreaterThanOrEqualTo, ColumnName = columnName10, Value = 33 }, + new() { Filter = FilterType.NotEqualTo, ColumnName = columnName11, Value = 44 }, + new() { Filter = FilterType.SmallerThan, ColumnName = columnName12, Value = 55 }, + new() { Filter = FilterType.SmallerThanOrEqualTo, ColumnName = columnName13, Value = 66 } + ]; + + // Act + var addIndexSql = Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [ + columnName1, + columnName2, + columnName3, + columnName4, + columnName5, + columnName6, + columnName7, + columnName8, + columnName9, + columnName10, + columnName11, + columnName12, + columnName13 + ], + Unique = false, + FilterItems = filterItems + }); + + // Assert + var indexesFromDatabase = Provider.GetIndexes(table: tableName); + + // In Oracle it seems that functional expressions are stored as index. FilterItems are not implemented in GetIndexes for Oracle. No further + // assert possible at this point in time. + Assert.That(indexesFromDatabase.Single().KeyColumns.Count, Is.EqualTo(13)); + + + var expectedSql = "CREATE INDEX TestIndexName ON TestTable (CASE WHEN TestColumn1 = 1 THEN TestColumn1 ELSE NULL END, CASE WHEN TestColumn2 > 2 THEN TestColumn2 ELSE NULL END, CASE WHEN TestColumn3 >= 2323 THEN TestColumn3 ELSE NULL END, CASE WHEN TestColumn4 <> 3434 THEN TestColumn4 ELSE NULL END, CASE WHEN TestColumn5 <> -3434 THEN TestColumn5 ELSE NULL END, CASE WHEN TestColumn6 < 3434345345 THEN TestColumn6 ELSE NULL END, CASE WHEN TestColumn7 <> 'asdf' THEN TestColumn7 ELSE NULL END, CASE WHEN TestColumn8 = 11 THEN TestColumn8 ELSE NULL END, CASE WHEN TestColumn9 > 22 THEN TestColumn9 ELSE NULL END, CASE WHEN TestColumn10 >= 33 THEN TestColumn10 ELSE NULL END, CASE WHEN TestColumn11 <> 44 THEN TestColumn11 ELSE NULL END, CASE WHEN TestColumn12 < 55 THEN TestColumn12 ELSE NULL END, CASE WHEN TestColumn13 <= 66 THEN TestColumn13 ELSE NULL END)"; + + Assert.That(addIndexSql, Is.EqualTo(expectedSql)); + } + + /// + /// Migrator throws if UNIQUE is used with functional expressions. + /// + [Test] + public void AddIndex_FilterItemsCombinedWithUnique_Throws() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int16) + ); + + List filterItems = [ + new() { Filter = FilterType.EqualTo, ColumnName = columnName1, Value = 1 }, + ]; + + // Act/Assert + Assert.Throws(() => Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [ + columnName1 + ], + Unique = true, + FilterItems = filterItems + })); + } } \ No newline at end of file diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index 5ef8ad29..053f9ca2 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -271,7 +271,7 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() ]; // Act - Provider.AddIndex(tableName, + var addIndexSql = Provider.AddIndex(tableName, new Index { Name = indexName, @@ -290,7 +290,7 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() columnName12, columnName13 ], - Unique = false, + Unique = true, FilterItems = filterItems }); @@ -311,5 +311,9 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) ); + + var expectedSql = "CREATE UNIQUE INDEX TestIndexName ON TestTable (TestColumn1, TestColumn2, TestColumn3, TestColumn4, TestColumn5, TestColumn6, TestColumn7, TestColumn8, TestColumn9, TestColumn10, TestColumn11, TestColumn12, TestColumn13) WHERE TestColumn1 = 1 AND TestColumn2 > 2 AND TestColumn3 >= 2323 AND TestColumn4 <> 3434 AND TestColumn5 <> -3434 AND TestColumn6 < 3434345345 AND TestColumn7 <> 'asdf' AND TestColumn8 = 11 AND TestColumn9 > 22 AND TestColumn10 >= 33 AND TestColumn11 <> 44 AND TestColumn12 < 55 AND TestColumn13 <= 66"; + + Assert.That(addIndexSql, Is.EqualTo(expectedSql)); } } diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index a9f52f30..f5ea162d 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -199,7 +199,7 @@ public void AddIndex_FilteredIndexSingle_Success() } /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) using unique are not supported in the migrator at this point in time. /// [Test] public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() @@ -274,7 +274,7 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() columnName12, columnName13 ], - Unique = false, + Unique = true, FilterItems = filterItems }); diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs index cf146f4c..7f400f4e 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -214,7 +214,7 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() ]; // Act - Provider.AddIndex(tableName, + var addIndexSql = Provider.AddIndex(tableName, new Index { Name = indexName, @@ -233,7 +233,7 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() columnName12, columnName13 ], - Unique = false, + Unique = true, FilterItems = filterItems }); @@ -254,6 +254,10 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) ); + + var expectedSql = "CREATE UNIQUE INDEX TestIndexName ON TestTable (TestColumn1, TestColumn2, TestColumn3, TestColumn4, TestColumn5, TestColumn6, TestColumn7, TestColumn8, TestColumn9, TestColumn10, TestColumn11, TestColumn12, TestColumn13) WHERE TestColumn1 = 1 AND TestColumn2 > 2 AND TestColumn3 >= 2323 AND TestColumn4 <> 3434 AND TestColumn5 <> -3434 AND TestColumn6 < 3434345345 AND TestColumn7 <> 'asdf' AND TestColumn8 = 11 AND TestColumn9 > 22 AND TestColumn10 >= 33 AND TestColumn11 <> 44 AND TestColumn12 < 55 AND TestColumn13 <= 66"; + + Assert.That(addIndexSql, Is.EqualTo(expectedSql)); } private string GetCreateIndexSqlString(string indexName) diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index 969dd091..ef4ffb33 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -390,6 +390,11 @@ public interface ITransformationProvider : IDisposable List ExecuteStringQuery(string sql, params object[] args); + /// + /// Oracle: The retrieval of filter items is not supported in this migrator. If functional expressions are used: they seem to be stored as separate columns (with generated names). + /// + /// + /// Index[] GetIndexes(string table); /// @@ -719,7 +724,7 @@ IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string /// Name of the database to delete void DropDatabases(string databaseName); - void AddIndex(string table, Index index); + string AddIndex(string table, Index index); /// /// Add a multi-column index to a table @@ -727,7 +732,7 @@ IDataReader SelectComplex(IDbCommand cmd, string table, string[] columns, string /// The name of the index to add. /// The name of the table that will get the index. /// The name of the column or columns that are in the index. - void AddIndex(string name, string table, params string[] columns); + string AddIndex(string name, string table, params string[] columns); /// /// Check to see if an index exists diff --git a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs index bf9eaaeb..77b50ae3 100644 --- a/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/Oracle/OracleTransformationProvider.cs @@ -136,25 +136,70 @@ public override void AddForeignKey(string name, string primaryTable, string[] pr ExecuteNonQuery(string.Format("ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4})", primaryTable, name, primaryColumnsSql, refTable, refColumnsSql)); } - public override void AddIndex(string table, Index index) + public override string AddIndex(string table, Index index) { ValidateIndex(tableName: table, index: index); + var hasFilterItems = index.FilterItems != null && index.FilterItems.Count > 0; - var hasIncludedColumns = index.IncludeColumns != null && index.IncludeColumns.Length > 0; - // Included columns and Clustered indexes are not supported in Oracle. We ignore the values given in the properties silently. + // Oracle does not support included columns and clustered indexes. We ignore the values given in the properties SILENTLY for backwards compatibility. + + if (index.Unique && hasFilterItems) + { + throw new MigrationException($"You cannot use unique together with functional expressions in Oracle ({nameof(FilterItem)})."); + } var name = QuoteConstraintNameIfRequired(index.Name); table = QuoteTableNameIfRequired(table); - var columns = QuoteColumnNamesIfRequired(index.KeyColumns); - var uniqueString = index.Unique ? "UNIQUE" : null; - var columnsString = $"({string.Join(", ", columns)})"; + List singleFilterStrings = []; - if (index.FilterItems != null && index.FilterItems.Count > 0) + + if (hasFilterItems) { - throw new NotSupportedException($"Oracle: This migrator does not support partial indexes for Oracle at this point in time. Please use 'if(Provider is {nameof(OracleTransformationProvider)})'"); + // In Oracle functional expressions replace the normal columns so we need to remove them + if (index.KeyColumns != null && index.KeyColumns.Length > 0) + { + var keyColumnsList = index.KeyColumns.ToList(); + + for (var i = keyColumnsList.Count - 1; i >= 0; i--) + { + if (index.FilterItems.Any(x => keyColumnsList[i].Equals(x.ColumnName, StringComparison.OrdinalIgnoreCase))) + { + keyColumnsList.RemoveAt(i); + } + } + + index.KeyColumns = keyColumnsList.ToArray(); + } + + foreach (var filterItem in index.FilterItems) + { + var comparisonString = _dialect.GetComparisonStringByFilterType(filterItem.Filter); + + var filterColumnQuoted = QuoteColumnNameIfRequired(filterItem.ColumnName); + string value = null; + + value = filterItem.Value switch + { + bool booleanValue => booleanValue ? "TRUE" : "FALSE", + string stringValue => $"'{stringValue}'", + byte or short or int or long => Convert.ToInt64(filterItem.Value).ToString(), + sbyte or ushort or uint or ulong => Convert.ToUInt64(filterItem.Value).ToString(), + _ => throw new NotImplementedException($"Given type in '{nameof(FilterItem)}' is not implemented. Please file an issue."), + }; + + var singleFilterString = $"CASE WHEN {filterColumnQuoted} {comparisonString} {value} THEN {filterColumnQuoted} ELSE NULL END"; + + singleFilterStrings.Add(singleFilterString); + } } + var mixedColumnNamesAndFilters = QuoteColumnNamesIfRequired(index.KeyColumns).ToList(); + mixedColumnNamesAndFilters.AddRange(singleFilterStrings); + var columnNamesAndFiltersString = $"({string.Join(", ", mixedColumnNamesAndFilters)})"; + + var uniqueString = index.Unique ? "UNIQUE" : null; + List list = []; list.Add("CREATE"); list.Add(uniqueString); @@ -162,13 +207,15 @@ public override void AddIndex(string table, Index index) list.Add(name); list.Add("ON"); list.Add(table); - list.Add(columnsString); + list.Add(columnNamesAndFiltersString); list = [.. list.Where(x => !string.IsNullOrWhiteSpace(x))]; var sql = string.Join(" ", list); ExecuteNonQuery(sql); + + return sql; } private void GuardAgainstMaximumIdentifierLengthForOracle(string name) @@ -892,8 +939,9 @@ LEFT JOIN user_constraints c ON i.index_name = c.index_name AND i.table_name = c.table_name WHERE - UPPER(i.table_name) = '{table.ToUpperInvariant()}' AND - i.index_type = 'NORMAL' + UPPER(i.table_name) = '{table.ToUpperInvariant()}' + -- AND + -- i.index_type = 'NORMAL' ORDER BY i.table_name, i.index_name, ic.column_position"; diff --git a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs index b2a03bf9..11221c75 100644 --- a/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/PostgreSQL/PostgreSQLTransformationProvider.cs @@ -60,7 +60,7 @@ protected override string GetPrimaryKeyConstraintName(string table) return reader.Read() ? reader.GetString(0) : null; } - public override void AddIndex(string table, Index index) + public override string AddIndex(string table, Index index) { ValidateIndex(tableName: table, index: index); @@ -121,6 +121,8 @@ public override void AddIndex(string table, Index index) var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); ExecuteNonQuery(sql); + + return sql; } public override Index[] GetIndexes(string table) diff --git a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs index fb02e0a4..dd5a9349 100644 --- a/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SQLite/SQLiteTransformationProvider.cs @@ -1415,7 +1415,7 @@ public override void AddTable(string name, string engine, params IDbField[] fiel } } - public override void AddIndex(string table, Index index) + public override string AddIndex(string table, Index index) { ValidateIndex(table, index); @@ -1478,6 +1478,8 @@ public override void AddIndex(string table, Index index) var sql = string.Join(" ", list.Where(x => !string.IsNullOrWhiteSpace(x))); ExecuteNonQuery(sql); + + return sql; } protected override string GetPrimaryKeyConstraintName(string table) diff --git a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs index 739e0f69..59b63ab5 100644 --- a/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs +++ b/src/Migrator/Providers/Impl/SqlServer/SqlServerTransformationProvider.cs @@ -138,7 +138,7 @@ public override void AddPrimaryKeyNonClustered(string name, string table, params string.Join(",", QuoteColumnNamesIfRequired(columns)))); } - public override void AddIndex(string table, Index index) + public override string AddIndex(string table, Index index) { ValidateIndex(tableName: table, index: index); @@ -210,6 +210,8 @@ public override void AddIndex(string table, Index index) var sql = string.Join(" ", list); ExecuteNonQuery(sql); + + return sql; } public override void ChangeColumn(string table, Column column) diff --git a/src/Migrator/Providers/NoOpTransformationProvider.cs b/src/Migrator/Providers/NoOpTransformationProvider.cs index b8c27e51..8da1b9b3 100644 --- a/src/Migrator/Providers/NoOpTransformationProvider.cs +++ b/src/Migrator/Providers/NoOpTransformationProvider.cs @@ -479,9 +479,11 @@ public void DropDatabases(string databaseName) } - public void AddIndex(string table, Index index) + public string AddIndex(string table, Index index) { + // Don't know what this is for... + return string.Empty; } public void Dispose() @@ -508,9 +510,13 @@ public void RemoveIndex(string table, string name) // No Op } - public void AddIndex(string name, string table, params string[] columns) + public string AddIndex(string name, string table, params string[] columns) { // No Op + + // Don't know what this is for... + + return string.Empty; } public bool IndexExists(string table, string name) diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 3199c40d..823f2e1e 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -2088,16 +2088,16 @@ public virtual void RemoveIndex(string table, string name) } } - public virtual void AddIndex(string table, Index index) + public virtual string AddIndex(string table, Index index) { throw new NotImplementedException($"{nameof(AddIndex)} is not overridden for the provider."); } - public virtual void AddIndex(string name, string table, params string[] columns) + public virtual string AddIndex(string name, string table, params string[] columns) { var index = new Index { Name = name, KeyColumns = columns }; - AddIndex(table, index); + return AddIndex(table, index); } protected string QuoteConstraintNameIfRequired(string name) From b708ea1c3983384280f8e4494f6b62463aab8a82 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 15:19:28 +0200 Subject: [PATCH 35/55] Add value. Just to be safe. --- .../OracleTransformationProvider_AddIndexTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index 06ce4b95..ee4cc993 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -134,6 +134,8 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() FilterItems = filterItems }); + Provider.Insert(table: tableName, [columnName1], [1]); + // Assert var indexesFromDatabase = Provider.GetIndexes(table: tableName); From 595441690a7774ef9b3cf7770bed3c1709d23486 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 15:20:46 +0200 Subject: [PATCH 36/55] Minor changes --- .../OracleTransformationProvider_AddIndexTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index ee4cc993..92d5291b 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -139,8 +139,8 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() // Assert var indexesFromDatabase = Provider.GetIndexes(table: tableName); - // In Oracle it seems that functional expressions are stored as index. FilterItems are not implemented in GetIndexes for Oracle. No further - // assert possible at this point in time. + // In Oracle it seems that functional expressions are stored as column with generated column name. FilterItems are not + // implemented in Provider.GetIndexes() for Oracle. No further assert possible at this point in time. Assert.That(indexesFromDatabase.Single().KeyColumns.Count, Is.EqualTo(13)); From a16bd46b8b6bd062458ac3d0fb24c011b6f81a9f Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 15:38:27 +0200 Subject: [PATCH 37/55] Minor changes --- .../SQLServer/SQLServerTransformationProvider_AddIndexTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index f5ea162d..ae469e4e 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -149,9 +149,6 @@ public void AddIndex_IncludeColumnsMultiple_Success() .Using((x, y) => string.Compare(x, y, ignoreCase: true))); } - /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. - /// [Test] public void AddIndex_FilteredIndexSingle_Success() { From 08afa05152a42145f3f0bbf4a8cfcf7e7e4735df Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 15:38:54 +0200 Subject: [PATCH 38/55] Minor changes --- .../PostgreSQLTransformationProvider_AddIndexTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index 053f9ca2..cb4b9099 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -165,9 +165,6 @@ public void AddIndex_IncludeColumnsMultiple_Success() .Using((x, y) => string.Compare(x, y, ignoreCase: true))); } - /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. - /// [Test] public void AddIndex_FilteredIndexSingle_Success() { From fd6c48ed687fb7fceaf815e3c9972d4a01de4d92 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 15:57:21 +0200 Subject: [PATCH 39/55] added sql statement equality check for SQL Server --- .../SQLServerTransformationProvider_AddIndexTests.cs | 7 +++++-- .../SQLite/SQLiteTransformationProvider_AddIndexTests.cs | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index ae469e4e..2e6a45e8 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -252,7 +252,7 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() ]; // Act - Provider.AddIndex(tableName, + var addIndexSql = Provider.AddIndex(tableName, new Index { Name = indexName, @@ -276,7 +276,6 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() }); // Assert - var indexesFromDatabase = Provider.GetIndexes(table: tableName); var filteredItemsFromDatabase = indexesFromDatabase.Single().FilterItems; @@ -292,5 +291,9 @@ public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() filteredItemsFromDatabase.Select(x => x.ColumnName.ToLowerInvariant()), Is.EquivalentTo(filterItems.Select(x => x.ColumnName.ToLowerInvariant())) ); + + var expectedSql = @"CREATE UNIQUE NONCLUSTERED INDEX [TestIndexName] ON [TestTable] ([TestColumn1], [TestColumn2], [TestColumn3], [TestColumn4], [TestColumn5], [TestColumn6], [TestColumn7], [TestColumn8], [TestColumn9], [TestColumn10], [TestColumn11], [TestColumn12], [TestColumn13]) WHERE [TestColumn1] = 1 AND [TestColumn2] > 2 AND [TestColumn3] >= 2323 AND [TestColumn4] <> 3434 AND [TestColumn5] <> -3434 AND [TestColumn6] < 3434345345 AND [TestColumn7] <> 'asdf' AND [TestColumn8] = 11 AND [TestColumn9] > 22 AND [TestColumn10] >= 33 AND [TestColumn11] <> 44 AND [TestColumn12] < 55 AND [TestColumn13] <= 66"; + + Assert.That(addIndexSql, Is.EqualTo(expectedSql)); } } diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs index 7f400f4e..33b8a1c9 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -108,9 +108,6 @@ public void AddIndex_FilteredIndexGreaterOrEqualThanNumber_Success() Assert.That(indexScriptFromDatabase, Is.EqualTo("CREATE UNIQUE INDEX TestIndexName ON TestTable (TestColumn, TestColumn2, TestColumn3) WHERE TestColumn >= 100 AND TestColumn2 = 'Hello' AND TestColumn3 = 1")); } - /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. - /// [Test] public void AddIndex_FilteredIndexSingle_Success() { From a1f19e48d68baed4cc39c40897075babc2f65b9a Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 16:09:09 +0200 Subject: [PATCH 40/55] Minor changes --- src/Migrator/Framework/Index.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Migrator/Framework/Index.cs b/src/Migrator/Framework/Index.cs index ac8c9f29..5376a944 100644 --- a/src/Migrator/Framework/Index.cs +++ b/src/Migrator/Framework/Index.cs @@ -39,7 +39,6 @@ public class Index : IDbField /// /// Gets or sets items that represent filter expressions in filtered indexes. Currently string, integer and boolean values are supported. /// Attention: In SQL Server the column used in the filter must be NOT NULL. - /// Oracle: Not supported for Oracle /// public List FilterItems { get; set; } = []; } From 4e899ccae5f05fa76693ce60a9bbe78014b38907 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 16:39:22 +0200 Subject: [PATCH 41/55] Minor changes --- src/Migrator/Framework/ForeignKeyConstraint.cs | 2 +- src/Migrator/Framework/ITransformationProvider.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Migrator/Framework/ForeignKeyConstraint.cs b/src/Migrator/Framework/ForeignKeyConstraint.cs index d12e8b2b..1a5af872 100644 --- a/src/Migrator/Framework/ForeignKeyConstraint.cs +++ b/src/Migrator/Framework/ForeignKeyConstraint.cs @@ -26,7 +26,7 @@ public ForeignKeyConstraint(string name, string parentTable, string[] parentcolu public string[] ChildColumns { get; set; } /// - /// Gets or sets the on update text. Currently only used for SQLite. + /// Gets or sets the on delete text. Currently only used for SQLite. /// public string OnDelete { get; set; } diff --git a/src/Migrator/Framework/ITransformationProvider.cs b/src/Migrator/Framework/ITransformationProvider.cs index ef4ffb33..477d61a5 100644 --- a/src/Migrator/Framework/ITransformationProvider.cs +++ b/src/Migrator/Framework/ITransformationProvider.cs @@ -403,7 +403,7 @@ public interface ITransformationProvider : IDisposable /// /// The table name that you want the columns for. /// - [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is nust a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] Column[] GetColumns(string table); /// @@ -415,13 +415,13 @@ public interface ITransformationProvider : IDisposable int GetColumnContentSize(string table, string columnName); /// - /// Get information about a single column in a table. + /// Gets information about a single column in a table. /// and can in some cases only be guessed. Do not rely on them. Same for /// /// The table name that you want the columns for. /// The column name for which you want information. /// - [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is nust a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] + [Obsolete("We cannot resolve the DbType or MigratorDbType exactly so the result is just a guess. Also the default value in the result is depending on DbType and therefore also a guess. Do not use this method any more. Look up the type in your migration history.")] Column GetColumnByName(string table, string column); /// From cc84015e8f770c328221d54f2dbe6db6141ced41 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 16:47:34 +0200 Subject: [PATCH 42/55] Minor change --- .../PostgreSQLTransformationProvider_AddIndexTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index cb4b9099..08290f09 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -63,8 +63,6 @@ public void AddIndex_Unique_Success() var index = indexes.Single(); Assert.That(index.Unique, Is.True); - // Need to compare message string since ErrorNumber does not hold a positive number. - Assert.That(ex.Message, Does.StartWith("23505: duplicate key value violates unique constraint")); Assert.That(ex.SqlState, Is.EqualTo("23505")); } From ed1b1b564f242047d1300f65b4bb8c743f8e4e93 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 16:52:07 +0200 Subject: [PATCH 43/55] Extended comments --- .../OracleTransformationProvider_AddIndexTests.cs | 3 ++- .../PostgreSQLTransformationProvider_AddIndexTests.cs | 3 ++- .../SQLServer/SQLServerTransformationProvider_AddIndexTests.cs | 3 ++- .../SQLite/SQLiteTransformationProvider_AddIndexTests.cs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs index 92d5291b..12ae1876 100644 --- a/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/OracleProvider/OracleTransformationProvider_AddIndexTests.cs @@ -55,7 +55,8 @@ public void AddIndex_Unique_Success() /// /// This test is located in the dedicated database type folder not in the base class since - /// cannot read filter items. + /// cannot read filter items for Oracle and Oracle does not allow + /// Unique = true for indexes with functional expressions /// [Test] public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() diff --git a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs index 08290f09..097a986c 100644 --- a/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/PostgreSQL/PostgreSQLTransformationProvider_AddIndexTests.cs @@ -210,7 +210,8 @@ public void AddIndex_FilteredIndexSingle_Success() } /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// This test is located in the dedicated database type folder not in the base class since + /// cannot read filter items for Oracle. /// [Test] public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() diff --git a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs index 2e6a45e8..91fb83f2 100644 --- a/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLServer/SQLServerTransformationProvider_AddIndexTests.cs @@ -196,7 +196,8 @@ public void AddIndex_FilteredIndexSingle_Success() } /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) using unique are not supported in the migrator at this point in time. + /// This test is located in the dedicated database type folder not in the base class since + /// cannot read filter items for Oracle. /// [Test] public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() diff --git a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs index 33b8a1c9..868a17b2 100644 --- a/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs +++ b/src/Migrator.Tests/Providers/SQLite/SQLiteTransformationProvider_AddIndexTests.cs @@ -155,7 +155,8 @@ public void AddIndex_FilteredIndexSingle_Success() } /// - /// This test is located in the dedicated database type folder not in the base class since partial indexes (Oracle) are not supported in the migrator at this point in time. + /// This test is located in the dedicated database type folder not in the base class since + /// cannot read filter items for Oracle. /// [Test] public void AddIndex_FilteredIndexMiscellaneousFilterTypesAndDataTypes_Success() From 73ac90fb8efffe4895370bf086b6d1c794e486fc Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Wed, 27 Aug 2025 20:08:42 +0200 Subject: [PATCH 44/55] Added some validation tests --- .../Generic/Generic_AddIndexTestsBase.cs | 60 ++++++++++++++++++- .../Providers/TransformationProvider.cs | 6 +- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs index 1fee4b54..6f7c3de8 100644 --- a/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs +++ b/src/Migrator.Tests/Providers/Generic/Generic_AddIndexTestsBase.cs @@ -22,6 +22,64 @@ public void AddIndex_TableDoesNotExist() Assert.Throws(() => Provider.AddIndex("NotExistingIndex", "NotExistingTable", "column")); } + [Test] + public void AddIndex_AddAlreadyExistingIndex_Throws() + { + // Arrange + const string tableName = "TestTable"; + const string columnName = "TestColumn"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName, DbType.Int32)); + Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName] }); + + // Act/Assert + // Add already existing index + Assert.Throws(() => Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName] })); + } + + [Test] + public void AddIndex_IncludeColumnsContainsColumnThatExistInKeyColumns_Throws() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, new Column(columnName1, DbType.Int32)); + + Assert.Throws(() => Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName1], + IncludeColumns = [columnName1] + })); + } + + [Test] + public void AddIndex_ColumnNameUsedInFilterItemDoesNotExistInKeyColumns_Throws() + { + // Arrange + const string tableName = "TestTable"; + const string columnName1 = "TestColumn1"; + const string columnName2 = "TestColumn2"; + const string indexName = "TestIndexName"; + + Provider.AddTable(tableName, + new Column(columnName1, DbType.Int32), + new Column(columnName2, DbType.Int32) + ); + + Assert.Throws(() => Provider.AddIndex(tableName, + new Index + { + Name = indexName, + KeyColumns = [columnName1], + FilterItems = [new FilterItem { Filter = FilterType.GreaterThan, ColumnName = columnName2, Value = 12 }] + })); + } + [Test] public void AddIndex_UsingIndexInstanceOverload_NonUnique_ShouldBeReadable() { @@ -30,7 +88,7 @@ public void AddIndex_UsingIndexInstanceOverload_NonUnique_ShouldBeReadable() const string columnName = "TestColumn"; const string indexName = "TestIndexName"; - Provider.AddTable(tableName, new Column(columnName, System.Data.DbType.Int32)); + Provider.AddTable(tableName, new Column(columnName, DbType.Int32)); // Act Provider.AddIndex(tableName, new Index { Name = indexName, KeyColumns = [columnName] }); diff --git a/src/Migrator/Providers/TransformationProvider.cs b/src/Migrator/Providers/TransformationProvider.cs index 823f2e1e..465af5a5 100644 --- a/src/Migrator/Providers/TransformationProvider.cs +++ b/src/Migrator/Providers/TransformationProvider.cs @@ -2199,7 +2199,7 @@ protected void ValidateIndex(string tableName, Index index) foreach (var keyColumn in index.KeyColumns) { - if (!columns.Any(x => x.Name.Equals(keyColumn, StringComparison.OrdinalIgnoreCase))) + if (!index.KeyColumns.All(x => columns.Any(y => y.Name.Equals(x, StringComparison.OrdinalIgnoreCase)))) { throw new MigrationException($"Column '{keyColumn}' does not exist."); } @@ -2207,9 +2207,9 @@ protected void ValidateIndex(string tableName, Index index) if (hasFilterItems) { - if (!index.KeyColumns.Any(x => index.FilterItems.Any(y => x.Equals(y.ColumnName, StringComparison.OrdinalIgnoreCase)))) + if (!index.FilterItems.All(x => index.KeyColumns.Any(y => x.ColumnName.Equals(y, StringComparison.OrdinalIgnoreCase)))) { - throw new MigrationException($"All columns in the {index.FilterItems} should exist in the {index.KeyColumns}."); + throw new MigrationException($"All columns in the {nameof(index.FilterItems)} should exist in the {nameof(index.KeyColumns)}."); } } From f2dee9593dbd1be079bc602d8065ff7f07ad1761 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 09:06:28 +0200 Subject: [PATCH 45/55] Dispose provider in tests --- src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs index f16ff560..44d85260 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs +++ b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs @@ -28,6 +28,7 @@ public virtual void TearDown() DropTestTables(); Provider?.Rollback(); + Provider?.Dispose(); } protected void DropTestTables() From 763ac99c96084acac3f0e9d9ca284b61e6dd251c Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 09:46:05 +0200 Subject: [PATCH 46/55] Dispose Postgre connection --- .../Base/TransformationProviderBase.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs index 44d85260..eae08dea 100644 --- a/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs +++ b/src/Migrator.Tests/Providers/Base/TransformationProviderBase.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Threading; using System.Threading.Tasks; using DotNetProjects.Migrator.Framework; @@ -13,6 +14,7 @@ using Migrator.Tests.Settings; using Migrator.Tests.Settings.Config; using Migrator.Tests.Settings.Models; +using Npgsql; using NUnit.Framework; namespace Migrator.Tests.Providers.Base; @@ -22,13 +24,24 @@ namespace Migrator.Tests.Providers.Base; /// public abstract class TransformationProviderBase { + private IDbConnection _dbConnection; + [TearDown] public virtual void TearDown() { DropTestTables(); Provider?.Rollback(); - Provider?.Dispose(); + + if (_dbConnection != null) + { + if (_dbConnection.State == ConnectionState.Open) + { + _dbConnection.Close(); + } + + _dbConnection.Dispose(); + } } protected void DropTestTables() @@ -109,7 +122,10 @@ protected async Task BeginPostgreSQLTransactionAsync() var postgreIntegrationTestService = databaseIntegrationTestServiceFactory.Create(DatabaseProviderType.Postgres); var databaseInfo = await postgreIntegrationTestService.CreateTestDatabaseAsync(databaseConnectionConfig, cts.Token); - Provider = new PostgreSQLTransformationProvider(new PostgreSQLDialect(), databaseInfo.DatabaseConnectionConfig.ConnectionString, null, "default", "Npgsql"); + + _dbConnection = new NpgsqlConnection(databaseInfo.DatabaseConnectionConfig.ConnectionString); + + Provider = new PostgreSQLTransformationProvider(new PostgreSQLDialect(), _dbConnection, null, "default", "Npgsql"); Provider.BeginTransaction(); await Task.CompletedTask; From a546cfcca102829fdbf0da886a8ba07003243ca2 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 10:05:10 +0200 Subject: [PATCH 47/55] Pooling for Postgres --- src/Migrator.Tests/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Migrator.Tests/appsettings.json b/src/Migrator.Tests/appsettings.json index 5bd3c423..e3727e08 100644 --- a/src/Migrator.Tests/appsettings.json +++ b/src/Migrator.Tests/appsettings.json @@ -10,7 +10,7 @@ }, { "Id": "PostgreSQL", - "ConnectionString": "Server=localhost;Port=5432;Database=testdb;User Id=testuser;Password=testpass;" + "ConnectionString": "Server=localhost;Port=5432;Database=testdb;User Id=testuser;Password=testpass;Pooling=true" }, { "Id": "Oracle", From bff84188276b209b4709017524ef60db3b3e48d6 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 10:14:50 +0200 Subject: [PATCH 48/55] Extended Postgre max connections --- .github/workflows/dotnetpull.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index c6616cc2..cced1c9a 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -32,6 +32,7 @@ jobs: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb + POSTGRES_MAX_CONNECTIONS: 1000 options: >- --health-cmd="pg_isready -U testuser" --health-interval=10s From d8d385f732cf70f9723f289e4a3601939e80aaee Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 12:09:20 +0200 Subject: [PATCH 49/55] Min pool size postgre --- src/Migrator.Tests/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Migrator.Tests/appsettings.json b/src/Migrator.Tests/appsettings.json index e3727e08..93c92dbc 100644 --- a/src/Migrator.Tests/appsettings.json +++ b/src/Migrator.Tests/appsettings.json @@ -10,7 +10,7 @@ }, { "Id": "PostgreSQL", - "ConnectionString": "Server=localhost;Port=5432;Database=testdb;User Id=testuser;Password=testpass;Pooling=true" + "ConnectionString": "Server=localhost;Port=5432;Database=testdb;User Id=testuser;Password=testpass;Pooling=true;Minimum Pool Size=10" }, { "Id": "Oracle", From 7b1788db1a88a06083511c51587df280cd1fb377 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 12:25:05 +0200 Subject: [PATCH 50/55] postgre max connections in env var --- .github/workflows/dotnetpull.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index cced1c9a..79ace774 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -25,14 +25,14 @@ jobs: --health-retries=10 postgres: - image: postgres:13 + image: postgres:16 ports: - 5432:5432 env: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb - POSTGRES_MAX_CONNECTIONS: 1000 + POSTGRES_INITDB_ARGS: "-c max_connections=300" options: >- --health-cmd="pg_isready -U testuser" --health-interval=10s From 4722a16d2b40beb3f3af2a9eb61a7081fb672171 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 13:23:39 +0200 Subject: [PATCH 51/55] Workaround for Postgre 13 --- .github/workflows/dotnetpull.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 79ace774..05d75bc7 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -32,7 +32,8 @@ jobs: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb - POSTGRES_INITDB_ARGS: "-c max_connections=300" + # As of v16 we can use: + # POSTGRES_INITDB_ARGS: "-c max_connections=300" options: >- --health-cmd="pg_isready -U testuser" --health-interval=10s @@ -85,6 +86,13 @@ jobs: - name: Create SQLServer database run: | /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Passw0rd' -Q "CREATE DATABASE [Whatever];" + - name: + Increase max connections (Postgre SQL). + # Postgre < 16 cannot adjust it using env vars + run: | + docker exec postgres bash -c "echo \"max_connections = 300\" >> /var/lib/postgresql/data/postgresql.conf" + docker restart postgres + sleep 5 - name: Create Oracle user run: | sql sys/adfkweflajdfglkj@localhost/FREEPDB1 as sysdba < Date: Thu, 28 Aug 2025 13:24:23 +0200 Subject: [PATCH 52/55] 16 => 13 --- .github/workflows/dotnetpull.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 05d75bc7..5230885b 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -25,7 +25,7 @@ jobs: --health-retries=10 postgres: - image: postgres:16 + image: postgres:13 ports: - 5432:5432 env: From 833474f06dd86bc16733671351b564c3579b8313 Mon Sep 17 00:00:00 2001 From: JaBistDuNarrisch Date: Thu, 28 Aug 2025 13:36:50 +0200 Subject: [PATCH 53/55] Workaround 2: use psql --- .github/workflows/dotnetpull.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 5230885b..f82a20c3 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -88,11 +88,10 @@ jobs: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Passw0rd' -Q "CREATE DATABASE [Whatever];" - name: Increase max connections (Postgre SQL). - # Postgre < 16 cannot adjust it using env vars + # Postgre < 16 cannot adjust it using env vars => Workaround: we use psql to adjust the max connections run: | - docker exec postgres bash -c "echo \"max_connections = 300\" >> /var/lib/postgresql/data/postgresql.conf" - docker restart postgres - sleep 5 + psql -h localhost -U postgres -d testdb -c "ALTER SYSTEM SET max_connections = 300;" + psql -h localhost -U postgres -d testdb -c "SELECT pg_reload_conf();" - name: Create Oracle user run: | sql sys/adfkweflajdfglkj@localhost/FREEPDB1 as sysdba < Date: Thu, 28 Aug 2025 13:44:52 +0200 Subject: [PATCH 54/55] Password for psql --- .github/workflows/dotnetpull.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index f82a20c3..0562799c 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -89,9 +89,11 @@ jobs: - name: Increase max connections (Postgre SQL). # Postgre < 16 cannot adjust it using env vars => Workaround: we use psql to adjust the max connections + env: + PGPASSWORD: testpass run: | - psql -h localhost -U postgres -d testdb -c "ALTER SYSTEM SET max_connections = 300;" - psql -h localhost -U postgres -d testdb -c "SELECT pg_reload_conf();" + psql -h localhost -U testuser -d testdb -c "ALTER SYSTEM SET max_connections = 300;" + psql -h localhost -U testuser -d testdb -c "SELECT pg_reload_conf();" - name: Create Oracle user run: | sql sys/adfkweflajdfglkj@localhost/FREEPDB1 as sysdba < Date: Thu, 28 Aug 2025 14:11:11 +0200 Subject: [PATCH 55/55] Use postgres as initial db --- .github/workflows/dotnetpull.yml | 15 --------------- .../PostgreSqlDatabaseIntegrationTestService.cs | 3 +-- src/Migrator.Tests/appsettings.json | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/dotnetpull.yml b/.github/workflows/dotnetpull.yml index 0562799c..2ed819de 100644 --- a/.github/workflows/dotnetpull.yml +++ b/.github/workflows/dotnetpull.yml @@ -5,11 +5,9 @@ on: branches: [master] pull_request: branches: [master] - jobs: build: runs-on: ubuntu-latest - services: sqlserver: image: mcr.microsoft.com/mssql/server:2019-latest @@ -23,7 +21,6 @@ jobs: --health-interval=10s --health-timeout=5s --health-retries=10 - postgres: image: postgres:13 ports: @@ -31,7 +28,6 @@ jobs: env: POSTGRES_USER: testuser POSTGRES_PASSWORD: testpass - POSTGRES_DB: testdb # As of v16 we can use: # POSTGRES_INITDB_ARGS: "-c max_connections=300" options: >- @@ -39,7 +35,6 @@ jobs: --health-interval=10s --health-timeout=5s --health-retries=5 - oracle: image: gvenzl/oracle-free:latest ports: @@ -51,7 +46,6 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 10 - mysql: image: mysql:8.0 ports: @@ -66,7 +60,6 @@ jobs: --health-interval=10s --health-timeout=5s --health-retries=10 - steps: - uses: actions/checkout@v4 - uses: gvenzl/setup-oracle-sqlcl@v1 @@ -86,14 +79,6 @@ jobs: - name: Create SQLServer database run: | /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Passw0rd' -Q "CREATE DATABASE [Whatever];" - - name: - Increase max connections (Postgre SQL). - # Postgre < 16 cannot adjust it using env vars => Workaround: we use psql to adjust the max connections - env: - PGPASSWORD: testpass - run: | - psql -h localhost -U testuser -d testdb -c "ALTER SYSTEM SET max_connections = 300;" - psql -h localhost -U testuser -d testdb -c "SELECT pg_reload_conf();" - name: Create Oracle user run: | sql sys/adfkweflajdfglkj@localhost/FREEPDB1 as sysdba < CreateTestDatabaseAsync(DatabaseConnect await context.ExecuteAsync($"CREATE DATABASE \"{newDatabaseName}\"", cancellationToken); } - var connectionStringBuilder2 = new NpgsqlConnectionStringBuilder + var connectionStringBuilder2 = new NpgsqlConnectionStringBuilder(clonedDatabaseConnectionConfig.ConnectionString) { - ConnectionString = clonedDatabaseConnectionConfig.ConnectionString, Database = newDatabaseName }; diff --git a/src/Migrator.Tests/appsettings.json b/src/Migrator.Tests/appsettings.json index 93c92dbc..d2710b6f 100644 --- a/src/Migrator.Tests/appsettings.json +++ b/src/Migrator.Tests/appsettings.json @@ -10,7 +10,7 @@ }, { "Id": "PostgreSQL", - "ConnectionString": "Server=localhost;Port=5432;Database=testdb;User Id=testuser;Password=testpass;Pooling=true;Minimum Pool Size=10" + "ConnectionString": "Server=localhost;Port=5432;Database=postgres;User Id=testuser;Password=testpass;Pooling=true;Minimum Pool Size=100" }, { "Id": "Oracle",