diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs index 0a19ae5c..488b9a1f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs @@ -216,6 +216,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement ((DatabaseInfo)databaseViewInfo.ObjectInfo).TargetRecoveryTimeInSec = smoDatabase.TargetRecoveryTime; // Files tab is only supported in SQL Server, but files exists for all servers and used in detach database, cannot depend on files property to check the supportability ((DatabaseInfo)databaseViewInfo.ObjectInfo).IsFilesTabSupported = true; + ((DatabaseInfo)databaseViewInfo.ObjectInfo).Filegroups = GetFileGroups(smoDatabase, databaseViewInfo); } if (prototype is DatabasePrototype160) @@ -225,9 +226,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement } databaseScopedConfigurationsCollection = smoDatabase.IsSupportedObject() ? smoDatabase.DatabaseScopedConfigurations : null; databaseViewInfo.FileTypesOptions = displayFileTypes.Values.ToArray(); - - // Get file groups names - GetFileGroupNames(smoDatabase, databaseViewInfo); } databaseViewInfo.DscOnOffOptions = DscOnOffOptions; databaseViewInfo.DscElevateOptions = DscElevateOptions; @@ -700,8 +698,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement if (!viewParams.IsNewObject && database.Files != null) { - HashSet fileIdsToRemove = new HashSet(prototype.Files.Select(file => file.ID)); - foreach (var file in database.Files) + Dictionary fileDict = new Dictionary(); + foreach (DatabaseFilePrototype file in prototype.Files) + { + fileDict[file.ID] = file; + } + foreach (DatabaseFile file in database.Files) { // Add a New file if (file.Id == 0) @@ -712,7 +714,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement newFile.PhysicalName = file.FileNameWithExtension; newFile.DatabaseFileType = fileTypesEnums[file.Type]; newFile.Exists = false; - newFile.Autogrowth = GetAutogrowth(prototype, file); + newFile.Autogrowth = GetAutogrowth(prototype, file); if (!string.IsNullOrEmpty(file.Path.Trim())) { newFile.Folder = Utility.DatabaseUtils.ConvertToLocalMachinePath(Path.GetFullPath(file.Path)); @@ -732,24 +734,69 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement // Edit file properties: updating the existed files with modified data else { - var existedFile = prototype.Files.FirstOrDefault(x => x.ID == file.Id); - if (existedFile != null) + DatabaseFilePrototype? existingFile; + if (fileDict.TryGetValue(file.Id, out existingFile)) { - fileIdsToRemove.Remove(file.Id); - existedFile.Name = file.Name; - existedFile.InitialSize = (int)file.SizeInMb; - existedFile.Autogrowth = GetAutogrowth(prototype, file); + existingFile.Name = file.Name; + existingFile.InitialSize = (int)file.SizeInMb; + existingFile.Autogrowth = GetAutogrowth(prototype, file); } + // Once updated, remove it from the dictionary + fileDict.Remove(file.Id); } } // Remove the file - foreach (var currentFile in prototype.Files) + foreach (KeyValuePair removedfile in fileDict) { - if (fileIdsToRemove.Contains(currentFile.ID)) + removedfile.Value.Removed = true; + } + } + + if (!viewParams.IsNewObject && database.Filegroups != null) + { + Dictionary groupNameDict = new Dictionary(); + foreach (FilegroupPrototype fileGroup in prototype.Filegroups) + { + groupNameDict[fileGroup.Name] = fileGroup; + } + // process row data filegroups + foreach (FileGroupSummary fg in database.Filegroups) + { + if (fg.Id < 0) { - currentFile.Removed = true; + FilegroupPrototype newfileGroup = new FilegroupPrototype(prototype); + newfileGroup.FileGroupType = fg.Type; + newfileGroup.Name = fg.Name; + newfileGroup.IsReadOnly = fg.IsReadOnly; + newfileGroup.IsDefault = fg.IsDefault; + newfileGroup.IsAutogrowAllFiles = fg.IsDefault; + prototype.Filegroups.Add(newfileGroup); } + else + { + FilegroupPrototype? existingFileGroup; + if (groupNameDict.TryGetValue(fg.Name, out existingFileGroup)) + { + if (fg.Type != FileGroupType.MemoryOptimizedDataFileGroup) + { + existingFileGroup.IsReadOnly = fg.IsReadOnly; + existingFileGroup.IsDefault = fg.IsDefault; + if (fg.Type != FileGroupType.FileStreamDataFileGroup) + { + existingFileGroup.IsAutogrowAllFiles = fg.AutogrowAllFiles; + } + } + // Once updated, remove it from the dictionary + groupNameDict.Remove(fg.Name); + } + } + } + + // Remove the filegroups + foreach (KeyValuePair removedFilegroups in groupNameDict) + { + removedFilegroups.Value.Removed = true; } } @@ -1018,35 +1065,26 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement return filesList.ToArray(); } - /// - /// Get the file group names from the database fileGroup + /// Preparing the filegroups of various FileGroupTypes /// - /// smo database prototype - /// database view info object - private void GetFileGroupNames(Database database, DatabaseViewInfo databaseViewInfo) + /// + /// + private FileGroupSummary[] GetFileGroups(Database database, DatabaseViewInfo databaseViewInfo) { - var rowDataGroups = new List(); - var fileStreamDataGroups = new List(); - foreach (FileGroup fileGroup in database.FileGroups) + var filegroups = new List(); + foreach (FileGroup filegroup in database.FileGroups) { - if (fileGroup.FileGroupType == FileGroupType.FileStreamDataFileGroup || fileGroup.FileGroupType == FileGroupType.MemoryOptimizedDataFileGroup) + filegroups.Add(new FileGroupSummary() { - fileStreamDataGroups.Add(fileGroup.Name); - } - else - { - rowDataGroups.Add(fileGroup.Name); - } + Id = filegroup.ID, + Name = filegroup.Name, + Type = filegroup.FileGroupType, + IsReadOnly = filegroup.ReadOnly, + IsDefault = filegroup.IsDefault + }); } - - // If no fileStream groups available - if (fileStreamDataGroups.Count == 0) - { - fileStreamDataGroups.Add(SR.prototype_file_noApplicableFileGroup); - } - databaseViewInfo.RowDataFileGroupsOptions = rowDataGroups.ToArray(); - databaseViewInfo.FileStreamFileGroupsOptions = fileStreamDataGroups.ToArray(); + return filegroups.ToArray(); } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs index 08d3c335..75df8fed 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs @@ -44,6 +44,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public DatabaseScopedConfigurationsInfo[]? DatabaseScopedConfigurations { get; set; } public bool? IsFilesTabSupported { get; set; } public DatabaseFile[] Files { get; set; } + public FileGroupSummary[]? Filegroups { get; set; } } public class DatabaseScopedConfigurationsInfo @@ -69,4 +70,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public double MaxSizeLimitInMb { get; set; } } + public class FileGroupSummary + { + public int Id { get; set; } + public string Name { get; set; } + public FileGroupType Type { get; set; } + public bool IsReadOnly { get; set; } + public bool IsDefault { get; set; } + public bool AutogrowAllFiles { get; set; } + } + } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs index 1d81772a..6595eaca 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseViewInfo.cs @@ -27,8 +27,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public string[] DscOnOffOptions { get; set; } public string[] DscElevateOptions { get; set; } public string[] DscEnableDisableOptions { get; set; } - public string[] RowDataFileGroupsOptions { get; set; } - public string[] FileStreamFileGroupsOptions { get; set; } public string[] FileTypesOptions { get; set; } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs index 5580dbdb..b829b3ef 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs @@ -22,6 +22,7 @@ using Microsoft.SqlTools.ServiceLayer.Test.Common; using NUnit.Framework; using static Microsoft.SqlTools.ServiceLayer.Admin.AzureSqlDbHelper; using DatabaseFile = Microsoft.SqlTools.ServiceLayer.ObjectManagement.DatabaseFile; +using FileGroup = Microsoft.SqlServer.Management.Smo.FileGroup; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement { @@ -305,10 +306,11 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).PageVerify, Is.EqualTo(testDatabase.PageVerify), $"PageVerify should match with testdata"); Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).RestrictAccess, Is.EqualTo(testDatabase.RestrictAccess), $"RestrictAccess should match with testdata"); Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).DatabaseScopedConfigurations, Is.Not.Null, $"DatabaseScopedConfigurations is not null"); - Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).DatabaseScopedConfigurations.Count, Is.GreaterThan(0), $"DatabaseScopedConfigurations should have at least a+ few properties"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).DatabaseScopedConfigurations?.Length, Is.GreaterThan(0), $"DatabaseScopedConfigurations should have at least a+ few properties"); Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).Files.Count, Is.EqualTo(2), $"Create database should create two database files"); Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).Files[0].Type, Is.EqualTo("ROWS Data"), $"Database files first file should be Row type database files"); Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).Files[1].Type, Is.EqualTo("LOG"), $"Database files first file should be Log type database files"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).Filegroups?.Length, Is.GreaterThan(0), $"Database file groups should exists"); // cleanup await ObjectManagementTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, objUrn, throwIfNotExist: true); @@ -438,6 +440,71 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement } } + /// + /// Adding, modifying and deleting database filegroups + /// /// + /// + [Test] + public async Task VerifyDatabaseFileGroupsTest() + { + using (var testDatabase = await SqlTestDb.CreateNewAsync(serverType: TestServerType.OnPrem, dbNamePrefix: "VerifyDatabaseFilegeoupsTest")) + { + var connectionResult = LiveConnectionHelper.InitLiveConnectionInfo(testDatabase.DatabaseName); + using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connectionResult.ConnectionInfo)) + { + var testDatabaseInfo = ObjectManagementTestUtils.GetTestDatabaseInfo(); + testDatabaseInfo.Name = testDatabase.DatabaseName; + testDatabaseInfo.CollationName = ""; + var objUrn = ObjectManagementTestUtils.GetDatabaseURN(testDatabase.DatabaseName); + + // Get database properties and verify + var parametersForUpdate = ObjectManagementTestUtils.GetInitializeViewRequestParams(connectionResult.ConnectionInfo.OwnerUri, testDatabase.DatabaseName, false, SqlObjectType.Database, "", objUrn); + DatabaseViewInfo databaseViewInfo = await ObjectManagementTestUtils.GetDatabaseObject(parametersForUpdate, testDatabaseInfo); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).Filegroups?.Length, Is.EqualTo(1), $"Create database should create one default database filegroup"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).Filegroups[0].Name, Is.EqualTo("PRIMARY"), $"Database default filegroup name should be PRIMARY"); + + List databaseFilegroup = new List(); + + // copy exisitng Row data files to the list + databaseFilegroup.Add(((DatabaseInfo)databaseViewInfo.ObjectInfo).Filegroups[0]); + + // Add new Filegroups + var testDatabaseFilegroups = ObjectManagementTestUtils.GetTestDatabaseFilegroups(); + databaseFilegroup.AddRange(testDatabaseFilegroups); + + // Attaching the filegroups to the testdatabase + testDatabaseInfo.Filegroups = databaseFilegroup.ToArray(); + + // Validate the result + await ObjectManagementTestUtils.SaveObject(parametersForUpdate, testDatabaseInfo); + DatabaseViewInfo updatedDatabaseViewInfo = await ObjectManagementTestUtils.GetDatabaseObject(parametersForUpdate, testDatabaseInfo); + + // verify the modified properties + Assert.That(((DatabaseInfo)updatedDatabaseViewInfo.ObjectInfo).Filegroups?.Length, Is.EqualTo(3), $"Four filegroups should exists, as we created three more"); + var filegroup = ((DatabaseInfo)updatedDatabaseViewInfo.ObjectInfo).Filegroups.FirstOrDefault(x => x.Name == databaseFilegroup[1].Name); + Assert.That(filegroup, Is.Not.Null, $"filegroup should exists"); + Assert.That(filegroup.Type, Is.EqualTo(FileGroupType.RowsFileGroup), $"Filegroup type should be matched"); + + filegroup = ((DatabaseInfo)updatedDatabaseViewInfo.ObjectInfo).Filegroups.FirstOrDefault(x => x.Name == databaseFilegroup[2].Name); + Assert.That(filegroup, Is.Not.Null, $"filegroup should exists"); + Assert.That(filegroup.Type, Is.EqualTo(FileGroupType.MemoryOptimizedDataFileGroup), $"Filegroup type should be matched"); + + // Deleting newly created file + List newfilegroups = ((DatabaseInfo)updatedDatabaseViewInfo.ObjectInfo).Filegroups.ToList(); + var fileIndexTobeRemoved = newfilegroups.FindIndex(x => x.Name == databaseFilegroup[1].Name); + newfilegroups.RemoveAt(fileIndexTobeRemoved); + testDatabaseInfo.Filegroups = newfilegroups.ToArray(); + + // Validate the result files + await ObjectManagementTestUtils.SaveObject(parametersForUpdate, testDatabaseInfo); + DatabaseViewInfo databaseViewInfoAfterFileDelete = await ObjectManagementTestUtils.GetDatabaseObject(parametersForUpdate, testDatabaseInfo); + Assert.That(((DatabaseInfo)databaseViewInfoAfterFileDelete.ObjectInfo).Filegroups.Count, Is.EqualTo(2), $"Should have only 3 filegroups as we removed one"); + filegroup = ((DatabaseInfo)databaseViewInfoAfterFileDelete.ObjectInfo).Filegroups.FirstOrDefault(x => x.Name == databaseFilegroup[1].Name); + Assert.That(filegroup, Is.Null, $"filegroup should exists"); + } + } + } + [Test] public async Task DetachDatabaseTest() diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs index 65bd577f..215cb5b6 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs @@ -127,7 +127,28 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement return databaseFiles.ToArray(); } - internal static UserInfo GetTestUserInfo(DatabaseUserType userType, string userName = null, string loginName = null) + internal static List GetTestDatabaseFilegroups() + { + List fgs = new List(); + fgs.Add(new FileGroupSummary() + { + Id = -1, + Name = "rowFilegroup1", + IsDefault = false, + IsReadOnly = false, + AutogrowAllFiles = true, + Type = FileGroupType.RowsFileGroup + }); + fgs.Add(new FileGroupSummary() + { + Id = -2, + Name = "memOptFg1", + Type = FileGroupType.MemoryOptimizedDataFileGroup + }); + return fgs; + } + + internal static UserInfo GetTestUserInfo(DatabaseUserType userType, string userName = null, string loginName = null) { return new UserInfo() {