From 5c7dae40e6a05ab132d886f2d9a671cb596b48ef Mon Sep 17 00:00:00 2001 From: Sai Avishkar Sreerama <74571829+ssreerama@users.noreply.github.com> Date: Wed, 21 Jun 2023 19:41:18 -0500 Subject: [PATCH] Enabling database properties general tab with real time values from SMO (#2093) * initial commit with all required db handler and props, also getting the data from ADS * database properties view updated * Delete Microsoft.SqlTools.ServiceLayer.sln This file should be ignored * Removed unwanted file * Using DatabaseHandler for properties as one handler per object * removed unused and unnecessary changes * minimal updates * moving type conversion to UI side, properties with original types. * conversion number fixed * Adding Localized strings * using existing objectUrn logic to get the smo object * Adding Integration tests for database properties verification * refactoring * updating test --- .../Localization/sr.cs | 11 +++++ .../Localization/sr.resx | 4 ++ .../Localization/sr.strings | 1 + .../Localization/sr.xlf | 5 ++ .../ObjectTypes/Database/DatabaseHandler.cs | 47 ++++++++++++++----- .../ObjectTypes/Database/DatabaseInfo.cs | 10 +++- .../Utility/DatabaseUtils.cs | 10 ++++ .../ObjectManagement/DatabaseHandlerTests.cs | 44 +++++++++++++++++ .../ObjectManagementTestUtils.cs | 24 +++++++++- 9 files changed, 141 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 5e06e131..d93f16b7 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -12429,6 +12429,14 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string databaseBackupDate_None + { + get + { + return Keys.GetString(Keys.databaseBackupDate_None); + } + } + public static string BasicAzureEdition { get @@ -17847,6 +17855,9 @@ namespace Microsoft.SqlTools.ServiceLayer public const string prototype_file_noApplicableFileGroup = "prototype_file_noApplicableFileGroup"; + public const string databaseBackupDate_None = "databaseBackupDate_None"; + + public const string BasicAzureEdition = "BasicAzureEdition"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 3d3f4621..eae3dac2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -6756,6 +6756,10 @@ The Query Processor estimates that implementing the following index could improv No Applicable Filegroup + + None + + Basic diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 54a20b8b..2883e583 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -2800,6 +2800,7 @@ general_containmentType_None = None general_containmentType_Partial = Partial filegroups_filestreamFiles = FILESTREAM Files prototype_file_noApplicableFileGroup = No Applicable Filegroup +databaseBackupDate_None = None ############################################################################ # Azure SQL DB diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index ca8843d0..5060a6fa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -8316,6 +8316,11 @@ The Query Processor estimates that implementing the following index could improv Hyperscale + + None + None + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs index 127d4b38..ef4b2293 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseHandler.cs @@ -98,7 +98,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public override Task InitializeObjectView(InitializeViewRequestParams requestParams) { // create a default data context and database object - using (var dataContainer = CreateDatabaseDataContainer(requestParams.ConnectionUri, ConfigAction.Create)) + ConfigAction configAction = requestParams.IsNewObject ? ConfigAction.Create : ConfigAction.Update; + var databaseInfo = !requestParams.IsNewObject ? new DatabaseInfo() { Name = requestParams.Database } : null; + using (var dataContainer = CreateDatabaseDataContainer(requestParams, configAction, databaseInfo)) { if (dataContainer.Server == null) { @@ -120,6 +122,27 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement IsAzureDB = isAzureDB }; + // Collect the Database properties information + if (!requestParams.IsNewObject) + { + var smoDatabase = dataContainer.SqlDialogSubject as Database; + databaseViewInfo.ObjectInfo = new DatabaseInfo() + { + Name = smoDatabase.Name, + CollationName = smoDatabase.Collation, + DateCreated = smoDatabase.CreateDate.ToString(), + LastDatabaseBackup = smoDatabase.LastBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastBackupDate.ToString(), + LastDatabaseLogBackup = smoDatabase.LastLogBackupDate == DateTime.MinValue ? SR.databaseBackupDate_None : smoDatabase.LastLogBackupDate.ToString(), + MemoryAllocatedToMemoryOptimizedObjectsInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.MemoryAllocatedToMemoryOptimizedObjectsInKB), + MemoryUsedByMemoryOptimizedObjectsInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.MemoryUsedByMemoryOptimizedObjectsInKB), + NumberOfUsers = smoDatabase.Users.Count, + Owner = smoDatabase.Owner, + SizeInMb = smoDatabase.Size, + SpaceAvailableInMb = DatabaseUtils.ConvertKbtoMb(smoDatabase.SpaceAvailable), + Status = smoDatabase.Status.ToString() + }; + } + // azure sql db doesn't have a sysadmin fixed role var compatibilityLevelEnabled = !isDw && (dataContainer.LoggedInUserIsSysadmin || isAzureDB); if (isAzureDB) @@ -200,7 +223,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public override Task Save(DatabaseViewContext context, DatabaseInfo obj) { ConfigureDatabase( - context.Parameters.ConnectionUri, + context.Parameters, obj, context.Parameters.IsNewObject ? ConfigAction.Create : ConfigAction.Update, RunType.RunNow); @@ -210,39 +233,37 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public override Task Script(DatabaseViewContext context, DatabaseInfo obj) { var script = ConfigureDatabase( - context.Parameters.ConnectionUri, + context.Parameters, obj, context.Parameters.IsNewObject ? ConfigAction.Create : ConfigAction.Update, RunType.ScriptToWindow); return Task.FromResult(script); } - private CDataContainer CreateDatabaseDataContainer(string connectionUri, ConfigAction configAction, DatabaseInfo? database = null) + private CDataContainer CreateDatabaseDataContainer(InitializeViewRequestParams requestParams, ConfigAction configAction, DatabaseInfo? database = null) { - ConnectionInfo connectionInfo = this.GetConnectionInfo(connectionUri); + ConnectionInfo connectionInfo = this.GetConnectionInfo(requestParams.ConnectionUri); CDataContainer dataContainer = CDataContainer.CreateDataContainer(connectionInfo, databaseExists: configAction != ConfigAction.Create); if (dataContainer.Server == null) { throw new InvalidOperationException(serverNotExistsError); } - string objectUrn = (configAction != ConfigAction.Create && database != null) - ? string.Format(System.Globalization.CultureInfo.InvariantCulture, - "Server/Database[@Name='{0}']", - Urn.EscapeString(database.Name)) - : string.Format(System.Globalization.CultureInfo.InvariantCulture, - "Server"); + string objectUrn = configAction != ConfigAction.Create && database != null + ? string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server/Database[@Name='{0}']", Urn.EscapeString(database.Name)) + : string.Format(System.Globalization.CultureInfo.InvariantCulture, "Server"); + dataContainer.SqlDialogSubject = dataContainer.Server.GetSmoObject(objectUrn); return dataContainer; } - private string ConfigureDatabase(string connectionUri, DatabaseInfo database, ConfigAction configAction, RunType runType) + private string ConfigureDatabase(InitializeViewRequestParams requestParams, DatabaseInfo database, ConfigAction configAction, RunType runType) { if (database.Name == null) { throw new ArgumentException("Database name not provided."); } - using (var dataContainer = CreateDatabaseDataContainer(connectionUri, configAction, database)) + using (var dataContainer = CreateDatabaseDataContainer(requestParams, configAction, database)) { if (dataContainer.Server == null) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs index 7063633e..8d0b43cf 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectManagement/ObjectTypes/Database/DatabaseInfo.cs @@ -15,7 +15,15 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectManagement public string? RecoveryModel { get; set; } public string? CompatibilityLevel { get; set; } public string? ContainmentType { get; set; } - + public string? DateCreated { get; set; } + public string? LastDatabaseBackup { get; set; } + public string? LastDatabaseLogBackup { get; set; } + public double? MemoryAllocatedToMemoryOptimizedObjectsInMb { get; set; } + public double? MemoryUsedByMemoryOptimizedObjectsInMb { get; set; } + public int? NumberOfUsers { get; set; } + public double? SizeInMb { get; set; } + public double? SpaceAvailableInMb { get; set; } + public string? Status { get; set; } public string? AzureBackupRedundancyLevel { get; set; } public string? AzureServiceLevelObjective { get; set; } public string? AzureEdition { get; set; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs index f02cd27d..12435d5c 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/DatabaseUtils.cs @@ -345,5 +345,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility return new string(nameChars); } private static readonly HashSet illegalFilenameCharacters = new HashSet(new char[] { '\\', '/', ':', '*', '?', '"', '<', '>', '|' }); + + /// + /// Converts value in KBs to MBs + /// + /// value in kilo bytes + /// Returns as double type + public static double ConvertKbtoMb(double valueInKb) + { + return (Math.Round(valueInKb / 1024, 2)); + } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs index 532cfb98..7728e529 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/DatabaseHandlerTests.cs @@ -260,6 +260,50 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement } } + [Test] + /// This test validates the newly created database properties and verifies with some default values + public async Task VerifyDatabasePropertiesTest() + { + // setup, drop database if exists. + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", serverType: TestServerType.OnPrem); + using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connectionResult.ConnectionInfo)) + { + var server = new Server(new ServerConnection(sqlConn)); + + var testDatabase = ObjectManagementTestUtils.GetTestDatabaseInfo(); + var objUrn = ObjectManagementTestUtils.GetDatabaseURN(testDatabase.Name); + await ObjectManagementTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, objUrn); + + try + { + // create database + var parametersForCreation = ObjectManagementTestUtils.GetInitializeViewRequestParams(connectionResult.ConnectionInfo.OwnerUri, "master", true, SqlObjectType.Database, "", ""); + await ObjectManagementTestUtils.SaveObject(parametersForCreation, testDatabase); + Assert.That(DatabaseExists(testDatabase.Name!, server), $"Expected database '{testDatabase.Name}' was not created succesfully"); + + // Get database properties and verify + var parametersForUpdate = ObjectManagementTestUtils.GetInitializeViewRequestParams(connectionResult.ConnectionInfo.OwnerUri, testDatabase.Name, false, SqlObjectType.Database, "", objUrn); + DatabaseViewInfo databaseViewInfo = await ObjectManagementTestUtils.GetDatabaseObject(parametersForUpdate, testDatabase); + Assert.That(databaseViewInfo.ObjectInfo, Is.Not.Null, $"Expected result should not be empty"); + Assert.That(databaseViewInfo.ObjectInfo.Name, Is.EqualTo(testDatabase.Name), $"database name should be matched"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).DateCreated, Is.Not.Null, $"database name should be matched"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).NumberOfUsers, Is.GreaterThan(0), $"Default database users count should not be zero"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).LastDatabaseBackup, Is.EqualTo(testDatabase.LastDatabaseBackup), $"Should have no database last backup date"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).LastDatabaseLogBackup, Is.EqualTo(testDatabase.LastDatabaseLogBackup), $"Should have no database backup log date"); + Assert.That(((DatabaseInfo)databaseViewInfo.ObjectInfo).SizeInMb, Is.GreaterThan(0), $"Should have default database size when created"); + + // cleanup + await ObjectManagementTestUtils.DropObject(connectionResult.ConnectionInfo.OwnerUri, objUrn, throwIfNotExist: true); + Assert.That(DatabaseExists(testDatabase.Name!, server), Is.False, $"Database '{testDatabase.Name}' was not dropped succesfully"); + } + finally + { + // Cleanup using SMO if Drop didn't work + DropDatabase(server, testDatabase.Name!); + } + } + } + private bool DatabaseExists(string dbName, Server server) { server.Databases.Refresh(); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs index 0026efdf..f863a419 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/ObjectManagement/ObjectManagementTestUtils.cs @@ -70,7 +70,9 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement CollationName = "SQL_Latin1_General_CP1_CI_AS", CompatibilityLevel = "SQL Server 2022 (160)", ContainmentType = "None", - RecoveryModel = "Full" + RecoveryModel = "Full", + LastDatabaseBackup = "None", + LastDatabaseLogBackup = "None" }; } @@ -152,6 +154,26 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.ObjectManagement await Service.HandleDisposeViewRequest(new DisposeViewRequestParams { ContextId = parameters.ContextId }, disposeViewRequestContext.Object); } + internal static async Task GetDatabaseObject(InitializeViewRequestParams parameters, SqlObject obj) + { + // Initialize the view + DatabaseViewInfo databaseViewInfo = new DatabaseViewInfo(); + var initViewRequestContext = new Mock>(); + initViewRequestContext + .Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(null)) + .Callback(r => databaseViewInfo = r); + await Service.HandleInitializeViewRequest(parameters, initViewRequestContext.Object); + + // Dispose the view + var disposeViewRequestContext = new Mock>(); + disposeViewRequestContext.Setup(x => x.SendResult(It.IsAny())) + .Returns(Task.FromResult(new DisposeViewRequestResponse())); + await Service.HandleDisposeViewRequest(new DisposeViewRequestParams { ContextId = parameters.ContextId }, disposeViewRequestContext.Object); + + return databaseViewInfo; + } + internal static async Task ScriptObject(InitializeViewRequestParams parameters, SqlObject obj) { // Initialize the view