diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 85f3b989..e0c43dbc 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -8,7 +8,7 @@ on: jobs: microsoft_sql: if: github.event.pull_request.draft == false - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: RunDockerTests: true steps: diff --git a/Directory.Build.props b/Directory.Build.props index f3e46ef3..12b5d042 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net8.0 TiCodeX - 2024.12.1 + 2025.3.1 true enable true diff --git a/SQLSchemaCompare.Core/Entities/Database/ABaseDbIndex.cs b/SQLSchemaCompare.Core/Entities/Database/ABaseDbIndex.cs index 70cb8f4e..f8f96b01 100644 --- a/SQLSchemaCompare.Core/Entities/Database/ABaseDbIndex.cs +++ b/SQLSchemaCompare.Core/Entities/Database/ABaseDbIndex.cs @@ -14,9 +14,19 @@ public class ABaseDbIndex : ABaseDbConstraint /// Used only by the DatabaseProvider to group the indexes and fill the ColumnDescending list public bool IsDescending { get; set; } + /// + /// Gets or sets a value indicating whether the index is included. + /// + public bool IsIncluded { get; set; } + /// /// Gets whether the column is descending, sorted like the ColumnNames list /// public List ColumnDescending { get; } = new List(); + + /// + /// Gets the included columns. + /// + public List IncludedColumns { get; } = new List(); } } diff --git a/SQLSchemaCompare.Infrastructure/DatabaseProviders/ADatabaseProvider.cs b/SQLSchemaCompare.Infrastructure/DatabaseProviders/ADatabaseProvider.cs index 958d897a..05067ab8 100644 --- a/SQLSchemaCompare.Infrastructure/DatabaseProviders/ADatabaseProvider.cs +++ b/SQLSchemaCompare.Infrastructure/DatabaseProviders/ADatabaseProvider.cs @@ -215,8 +215,9 @@ protected TDatabase DiscoverDatabase(TDatabaseContext context, TaskInfo taskInfo { var index = indexGroup.First(); index.Database = db; - index.ColumnNames.AddRange(indexGroup.OrderBy(x => x.OrdinalPosition).Select(x => x.ColumnName)); - index.ColumnDescending.AddRange(indexGroup.OrderBy(x => x.OrdinalPosition).Select(x => x.IsDescending)); + index.ColumnNames.AddRange(indexGroup.Where(x => !x.IsIncluded).OrderBy(x => x.OrdinalPosition).Select(x => x.ColumnName)); + index.ColumnDescending.AddRange(indexGroup.Where(x => !x.IsIncluded).OrderBy(x => x.OrdinalPosition).Select(x => x.IsDescending)); + index.IncludedColumns.AddRange(indexGroup.Where(x => x.IsIncluded).OrderBy(x => x.OrdinalPosition).Select(x => x.ColumnName)); db.Indexes.Add(index); } } diff --git a/SQLSchemaCompare.Infrastructure/DatabaseProviders/MicrosoftSqlDatabaseProvider.cs b/SQLSchemaCompare.Infrastructure/DatabaseProviders/MicrosoftSqlDatabaseProvider.cs index f52ac6bb..b9d7a105 100644 --- a/SQLSchemaCompare.Infrastructure/DatabaseProviders/MicrosoftSqlDatabaseProvider.cs +++ b/SQLSchemaCompare.Infrastructure/DatabaseProviders/MicrosoftSqlDatabaseProvider.cs @@ -207,6 +207,7 @@ protected override IEnumerable GetIndexes(MicrosoftSqlDatabaseCont query.AppendLine(" CAST(ic.key_ordinal AS bigint) AS 'OrdinalPosition',"); query.AppendLine(" i.type AS Type,"); query.AppendLine(" i.is_unique AS 'IsUnique',"); + query.AppendLine(" ic.is_included_column AS 'IsIncluded',"); query.AppendLine(" i.filter_definition AS 'FilterDefinition'"); query.AppendLine("FROM sys.indexes i"); query.AppendLine("JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id"); diff --git a/SQLSchemaCompare.Infrastructure/SqlScripters/MicrosoftSqlScripter.cs b/SQLSchemaCompare.Infrastructure/SqlScripters/MicrosoftSqlScripter.cs index c863145f..060dcd70 100644 --- a/SQLSchemaCompare.Infrastructure/SqlScripters/MicrosoftSqlScripter.cs +++ b/SQLSchemaCompare.Infrastructure/SqlScripters/MicrosoftSqlScripter.cs @@ -360,6 +360,12 @@ protected override string ScriptCreateIndex(ABaseDbIndex index) } sb.AppendLine($"INDEX {this.ScriptHelper.ScriptObjectName(index.Name)} ON {this.ScriptHelper.ScriptObjectName(index.TableSchema, index.TableName)}({string.Join(",", columnList)})"); + + if (index.IncludedColumns.Any()) + { + sb.AppendLine($"INCLUDE({string.Join(",", index.IncludedColumns.Select(this.ScriptHelper.ScriptObjectName))})"); + } + if (!string.IsNullOrWhiteSpace(indexMicrosoft.FilterDefinition)) { sb.AppendLine($"{Indent}WHERE {indexMicrosoft.FilterDefinition}"); diff --git a/SQLSchemaCompare.Test/Datasources/sakila-schema-microsoftsql.sql b/SQLSchemaCompare.Test/Datasources/sakila-schema-microsoftsql.sql index 3daa00c0..efbddb04 100644 --- a/SQLSchemaCompare.Test/Datasources/sakila-schema-microsoftsql.sql +++ b/SQLSchemaCompare.Test/Datasources/sakila-schema-microsoftsql.sql @@ -136,6 +136,8 @@ ALTER TABLE customer_data.address ADD CONSTRAINT [DF_address_last_update] DEFAUL GO CREATE INDEX idx_fk_city_id ON customer_data.address(city_id) GO +CREATE INDEX idx_address_address2_district_postal_code ON customer_data.address(address, address2) INCLUDE(district, postal_code) +GO ALTER TABLE customer_data.address WITH NOCHECK ADD CONSTRAINT fk_address_city FOREIGN KEY (city_id) REFERENCES customer_data.city (city_id) ON DELETE NO ACTION ON UPDATE CASCADE GO ALTER TABLE customer_data.address NOCHECK CONSTRAINT fk_address_city diff --git a/SQLSchemaCompare.Test/Integration/MicrosoftSqlTests.cs b/SQLSchemaCompare.Test/Integration/MicrosoftSqlTests.cs index 6abae878..9dee3793 100644 --- a/SQLSchemaCompare.Test/Integration/MicrosoftSqlTests.cs +++ b/SQLSchemaCompare.Test/Integration/MicrosoftSqlTests.cs @@ -440,6 +440,21 @@ public void MigrateMicrosoftSqlDatabaseTargetMissingIndex(ushort port) this.dbFixture.AlterTargetDatabaseExecuteFullAndAllAlterScriptsAndCompare(DatabaseType.MicrosoftSql, sb.ToString(), port); } + /// + /// Test migration script when target db doesn't have a index with included columns + /// + /// The port of the server + [Theory] + [MemberData(nameof(DatabaseFixtureMicrosoftSql.ServerPorts), MemberType = typeof(DatabaseFixtureMicrosoftSql))] + [IntegrationTest] + [Category("MicrosoftSQL")] + public void MigrateMicrosoftSqlDatabaseTargetMissingIndexWithIncludedColumns(ushort port) + { + var sb = new StringBuilder(); + sb.AppendLine("DROP INDEX idx_address_address2_district_postal_code ON customer_data.address"); + this.dbFixture.AlterTargetDatabaseExecuteFullAndAllAlterScriptsAndCompare(DatabaseType.MicrosoftSql, sb.ToString(), port); + } + /// /// Test migration script when target db have an additional index /// @@ -455,6 +470,21 @@ public void MigrateMicrosoftSqlDatabaseTargetExtraIndex(ushort port) this.dbFixture.AlterTargetDatabaseExecuteFullAndAllAlterScriptsAndCompare(DatabaseType.MicrosoftSql, sb.ToString(), port); } + /// + /// Test migration script when target db have an additional index with included columns + /// + /// The port of the server + [Theory] + [MemberData(nameof(DatabaseFixtureMicrosoftSql.ServerPorts), MemberType = typeof(DatabaseFixtureMicrosoftSql))] + [IntegrationTest] + [Category("MicrosoftSQL")] + public void MigrateMicrosoftSqlDatabaseTargetExtraIndexWithIncludedColumns(ushort port) + { + var sb = new StringBuilder(); + sb.AppendLine("CREATE INDEX idx_title_release_year_special_features ON inventory.film (title, release_year) INCLUDE (special_features)"); + this.dbFixture.AlterTargetDatabaseExecuteFullAndAllAlterScriptsAndCompare(DatabaseType.MicrosoftSql, sb.ToString(), port); + } + /// /// Test migration script when target db have a different filtered index /// @@ -487,6 +517,22 @@ public void MigrateMicrosoftSqlDatabaseTargetDifferentIndexType(ushort port) this.dbFixture.AlterTargetDatabaseExecuteFullAndAllAlterScriptsAndCompare(DatabaseType.MicrosoftSql, sb.ToString(), port); } + /// + /// Test migration script when target db have a different index included columns + /// + /// The port of the server + [Theory] + [MemberData(nameof(DatabaseFixtureMicrosoftSql.ServerPorts), MemberType = typeof(DatabaseFixtureMicrosoftSql))] + [IntegrationTest] + [Category("MicrosoftSQL")] + public void MigrateMicrosoftSqlDatabaseTargetDifferentIndexIncludedColumns(ushort port) + { + var sb = new StringBuilder(); + sb.AppendLine("DROP INDEX idx_address_address2_district_postal_code ON customer_data.address"); + sb.AppendLine("CREATE INDEX idx_address_address2_district_postal_code ON customer_data.address(address, address2) INCLUDE(district, postal_code, phone)"); + this.dbFixture.AlterTargetDatabaseExecuteFullAndAllAlterScriptsAndCompare(DatabaseType.MicrosoftSql, sb.ToString(), port); + } + /// /// Test migration script when target db doesn't have a trigger /// diff --git a/SQLSchemaCompare/package.json b/SQLSchemaCompare/package.json index ad939325..4334438b 100644 --- a/SQLSchemaCompare/package.json +++ b/SQLSchemaCompare/package.json @@ -1,6 +1,6 @@ { "name": "sqlschemacompare", - "version": "2024.12.1", + "version": "2025.3.1", "license": "GPL-3.0-only", "description": "The Swiss Army Knife of Database Schema Comparison for Microsoft SQL, mySQL and PostgreSQL which runs on Windows, Linux and macOS systems.", "main": "app.js",