diff --git a/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/ReliableConnectionHelper.cs b/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/ReliableConnectionHelper.cs index 31f253bb..c684e86c 100644 --- a/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/ReliableConnectionHelper.cs +++ b/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/ReliableConnectionHelper.cs @@ -652,6 +652,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection public string OsVersion; public string MachineName; + public string ServerName; public Dictionary Options { get; set; } } @@ -666,6 +667,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection public int Port; } + public class ServerHostInfo + { + public string Platform; + public string Distribution; + public string Release; + public string ServicePackLevel; + } + public static bool TryGetServerVersion(string connectionString, out ServerInfo serverInfo, string azureAccountToken) { serverInfo = null; @@ -697,6 +706,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection azureAccountToken: azureAccountToken); return serverInfo; + } + + /// + /// Gets the server host information from sys.dm_os_host_info view + /// + /// The connection + public static ServerHostInfo GetServerHostInfo(IDbConnection connection) + { + // SQL Server 2016 and below does not provide sys.dm_os_host_info + if (!Version.TryParse(ReadServerVersion(connection), out var hostVersion) || hostVersion.Major <= 13) + { + return new ServerHostInfo + { + Platform = "Windows" + }; + } + + var hostInfo = new ServerHostInfo(); + ExecuteReader( + connection, + SqlConnectionHelperScripts.GetHostInfo, + reader => + { + reader.Read(); + hostInfo.Platform = reader[0].ToString(); + hostInfo.Distribution = reader[1].ToString(); + hostInfo.Release = reader[2].ToString(); + hostInfo.ServicePackLevel = reader[3].ToString(); + }); + + return hostInfo; } /// @@ -729,11 +769,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection serverInfo.ServerLevel = reader[2].ToString(); serverInfo.ServerEdition = reader[3].ToString(); serverInfo.MachineName = reader[4].ToString(); + serverInfo.ServerName = reader[5].ToString(); - if (reader.FieldCount > 5) + if (reader.FieldCount > 6) { // Detect the presence of SXI - serverInfo.IsSelectiveXmlIndexMetadataPresent = reader.GetInt32(5) == 1; + serverInfo.IsSelectiveXmlIndexMetadataPresent = reader.GetInt32(6) == 1; } // The 'ProductVersion' server property is of the form ##.#[#].####.#, diff --git a/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/SqlConnectionHelperScripts.cs b/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/SqlConnectionHelperScripts.cs index 19dd5f54..c19815aa 100644 --- a/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/SqlConnectionHelperScripts.cs +++ b/src/Microsoft.SqlTools.ManagedBatchParser/ReliableConnection/SqlConnectionHelperScripts.cs @@ -7,8 +7,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection { static class SqlConnectionHelperScripts { - public const string EngineEdition = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WITH (NOLOCK) WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)"; - public const string EngineEditionWithLock = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)"; + public const string EngineEdition = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), SERVERPROPERTY ('ServerName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WITH (NOLOCK) WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)"; + public const string EngineEditionWithLock = "SELECT SERVERPROPERTY('EngineEdition'), SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'), SERVERPROPERTY ('MachineName'), SERVERPROPERTY ('ServerName'), (SELECT CASE WHEN EXISTS (SELECT TOP 1 1 from [sys].[all_columns] WHERE name = N'xml_index_type' AND OBJECT_ID(N'sys.xml_indexes') = object_id) THEN 1 ELSE 0 END AS SXI_PRESENT)"; public const string CheckDatabaseReadonly = @"EXEC sp_dboption '{0}', 'read only'"; @@ -48,5 +48,6 @@ SELECT @filepath AS FilePath public const string GetOsVersion = @"SELECT OSVersion = RIGHT(@@version, LEN(@@version)- 3 -charindex (' on ', LOWER(@@version)))"; public const string GetClusterEndpoints = @"SELECT [name], [description], [endpoint], [protocol_desc] FROM .[sys].[dm_cluster_endpoints];"; + public const string GetHostInfo = @"SELECT [host_platform], [host_distribution], [host_release], [host_service_pack_level], [host_sku], [os_language_version] FROM sys.dm_os_host_info"; } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index bcbd9993..de0da135 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -26,6 +26,7 @@ using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.SchemaCompare; using Microsoft.SqlTools.ServiceLayer.Scripting; using Microsoft.SqlTools.ServiceLayer.Security; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.ServiceLayer.Workspace; @@ -129,6 +130,9 @@ namespace Microsoft.SqlTools.ServiceLayer ExternalLanguageService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(ExternalLanguageService.Instance); + + SqlAssessmentService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(SqlAssessmentService.Instance); InitializeHostedServices(serviceProvider, serviceHost); serviceHost.ServiceProvider = serviceProvider; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 696f6ed3..e8fcbac5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -2981,6 +2981,30 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string SqlAssessmentGenerateScriptTaskName + { + get + { + return Keys.GetString(Keys.SqlAssessmentGenerateScriptTaskName); + } + } + + public static string SqlAssessmentQueryInvalidOwnerUri + { + get + { + return Keys.GetString(Keys.SqlAssessmentQueryInvalidOwnerUri); + } + } + + public static string SqlAssessmentConnectingError + { + get + { + return Keys.GetString(Keys.SqlAssessmentConnectingError); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -3156,6 +3180,11 @@ namespace Microsoft.SqlTools.ServiceLayer return Keys.GetString(Keys.ScheduleNameAlreadyExists, scheduleName); } + public static string SqlAssessmentUnsuppoertedEdition(int editionCode) + { + return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode); + } + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Keys { @@ -4376,6 +4405,18 @@ namespace Microsoft.SqlTools.ServiceLayer public const string SchemaCompareSessionNotFound = "SchemaCompareSessionNotFound"; + public const string SqlAssessmentGenerateScriptTaskName = "SqlAssessmentGenerateScriptTaskName"; + + + public const string SqlAssessmentQueryInvalidOwnerUri = "SqlAssessmentQueryInvalidOwnerUri"; + + + public const string SqlAssessmentConnectingError = "SqlAssessmentConnectingError"; + + + public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 56e08a60..c656689d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1768,4 +1768,21 @@ Could not find the schema compare session to cancel + + Generate SQL Assessment script + + + + Not connected to a server + + + + Cannot connect to the server + + + + Unsupported engine edition {0} + . + Parameters: 0 - editionCode (int) + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 3391f094..1773a7c5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -816,4 +816,12 @@ ExtractInvalidVersion = Invalid version '{0}' passed. Version must be in the for PublishChangesTaskName = Apply schema compare changes SchemaCompareExcludeIncludeNodeNotFound = Failed to find the specified change in the model OpenScmpConnectionBasedModelParsingError = Error encountered while trying to parse connection information for endpoint '{0}' with error message '{1}' -SchemaCompareSessionNotFound = Could not find the schema compare session to cancel \ No newline at end of file +SchemaCompareSessionNotFound = Could not find the schema compare session to cancel + +############################################################################ +# SQL Assessment + +SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script +SqlAssessmentQueryInvalidOwnerUri = Not connected to a server +SqlAssessmentConnectingError = Cannot connect to the server +SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0} \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 10139a7c..eab50707 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2056,6 +2056,32 @@ Encountered unsupported token {0} + + A SQL Assessment operation's Execute method should not be called more than once + A SQL Assessment operation's Execute method should not be called more than once + + + + Generate SQL Assessment script + Generate SQL Assessment script + + + + Not connected to a server + Not connected to a server + + + + Cannot connect to the server + Cannot connect to the server + + + + Unsupported engine edition {0} + Unsupported engine edition {0} + . + Parameters: 0 - editionCode (int) + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/AssessmentRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/AssessmentRequest.cs new file mode 100644 index 00000000..c360d854 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/AssessmentRequest.cs @@ -0,0 +1,129 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; + +using Microsoft.SqlServer.Management.Assessment; + +namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts +{ + /// + /// Parameters for executing a query from a provided string + /// + public class AssessmentParams + { + /// + /// Gets or sets the owner uri to get connection from + /// + public string OwnerUri { get; set; } + + /// + /// Gets or sets the target type + /// + public SqlObjectType TargetType { get; set; } + } + + /// + /// Describes an item returned by SQL Assessment RPC methods + /// + public class AssessmentItemInfo + { + /// + /// Gets or sets assessment ruleset version. + /// + public string RulesetVersion { get; set; } + + /// + /// Gets or sets assessment ruleset name + /// + public string RulesetName { get; set; } + + /// + /// Gets or sets assessed target's type. + /// Supported values: 1 - server, 2 - database. + /// + public SqlObjectType TargetType { get; set; } + + /// + /// Gets or sets the assessed object's name. + /// + public string TargetName { get; set; } + + /// + /// Gets or sets check's ID. + /// + public string CheckId { get; set; } + + /// + /// Gets or sets tags assigned to this item. + /// + public string[] Tags { get; set; } + + /// + /// Gets or sets a display name for this item. + /// + public string DisplayName { get; set; } + + /// + /// Gets or sets a brief description of the item's purpose. + /// + public string Description { get; set; } + + /// + /// Gets or sets a containing + /// an link to a page providing detailed explanation + /// of the best practice. + /// + public string HelpLink { get; set; } + + /// + /// Gets or sets a indicating + /// severity level assigned to this items. + /// Values are: "Information", "Warning", "Critical". + /// + public string Level { get; set; } + } + + /// + /// Generic SQL Assessment Result + /// + /// + /// Result item's type derived from + /// + public class AssessmentResultData + where T : AssessmentItemInfo + { + /// + /// Gets the collection of assessment results. + /// + public List Items { get; } = new List(); + + /// + /// Gets or sets SQL Assessment API version. + /// + public string ApiVersion { get; set; } + } + + /// + /// Generic SQL Assessment Result + /// + /// + /// Result item's type derived from + /// + public class AssessmentResult : AssessmentResultData + where T : AssessmentItemInfo + { + /// + /// Gets or sets a value indicating + /// if assessment operation was successful. + /// + public bool Success { get; set; } + + /// + /// Gets or sets an status message for the operation. + /// + public string ErrorMessage { get; set; } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/GenerateScriptRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/GenerateScriptRequest.cs new file mode 100644 index 00000000..ead2b543 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/GenerateScriptRequest.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts +{ + /// + /// Parameters for executing a query from a provided string + /// + public class GenerateScriptParams + { + /// + /// Gets or sets a list of assessment result items + /// to be written to a table + /// + public List Items { get; set; } + + public TaskExecutionMode TaskExecutionMode { get; set; } + + public string TargetServerName { get; set; } + + public string TargetDatabaseName { get; set; } + } + + public class GenerateScriptResult + { + /// + /// Gets or sets a value indicating + /// if assessment operation was successful + /// + public bool Success { get; set; } + + /// + /// Gets or sets an status message for the operation + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets script text + /// + public string Script { get; set; } + } + + public class GenerateScriptRequest + { + public static readonly + RequestType Type = + RequestType.Create("assessment/generateScript"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/GetAssessmentItemsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/GetAssessmentItemsRequest.cs new file mode 100644 index 00000000..b194da7e --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/GetAssessmentItemsRequest.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Protocol.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts +{ + /// + /// Parameters for executing a query from a provided string + /// + public class GetAssessmentItemsParams : AssessmentParams + { + // a placeholder for future specialization + } + + /// + /// Describes a check used to assess SQL Server objects. + /// + public class CheckInfo : AssessmentItemInfo + { + // a placeholder for future specialization + } + + + public class GetAssessmentItemsRequest + { + public static readonly RequestType> Type = + RequestType>.Create( + "assessment/getAssessmentItems"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/InvokeRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/InvokeRequest.cs new file mode 100644 index 00000000..6df4a01d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/Contracts/InvokeRequest.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts +{ + /// + /// Parameters for executing a query from a provided string + /// + public class InvokeParams : AssessmentParams + { + // a placeholder for future specialization + } + + /// + /// SQL Assessment result item kind. + /// + /// + /// SQL Assessment run is a set of checks. Every check + /// may return a result item. Normally it is a note containing + /// recommendations on improving target's configuration. + /// But some checks may fail to obtain data due to access + /// restrictions or data integrity. In this case + /// the check produces an error or a warning. + /// + public enum AssessmentResultItemKind + { + /// + /// SQL Assessment item contains recommendation + /// + Note = 0, + + /// + /// SQL Assessment item contains a warning on + /// limited assessment capabilities + /// + Warning = 1, + + /// + /// SQL Assessment item contain a description of + /// error occured in the course of assessment run + /// + Error = 2 + } + + /// + /// Describes an assessment result item + /// containing a recommendation based on best practices. + /// + public class AssessmentResultItem : AssessmentItemInfo + { + /// + /// Gets or sets a message to the user + /// containing the recommendation. + /// + public string Message { get; set; } + + /// + /// Gets or sets result type: + /// 0 - real result, 1 - warning, 2 - error. + /// + public AssessmentResultItemKind Kind { get; set; } + + /// + /// Gets or sets date and time + /// when the item had been acquired. + /// + public DateTimeOffset Timestamp { get; set; } + } + + public class InvokeRequest + { + public static readonly + RequestType> Type = + RequestType>.Create("assessment/invoke"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/GenerateScriptOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/GenerateScriptOperation.cs new file mode 100644 index 00000000..65a1a0b3 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/GenerateScriptOperation.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using System.Threading; +using Microsoft.SqlTools.ServiceLayer.Management; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment +{ + /// + /// Generates a script storing SQL Assessment results to a table. + /// + internal sealed class GenerateScriptOperation : ITaskOperation, IDisposable + { + private readonly CancellationTokenSource cancellation = new CancellationTokenSource(); + + private bool disposed = false; + + /// + /// Gets the unique id associated with this instance. + /// + public string OperationId { get; set; } + + /// + /// Gets the parameters containing assessment results + /// to be stored in a data table. + /// + public GenerateScriptParams Parameters { get; } + + /// + /// Gets or sets the error message text + /// if an error occurred during task execution + /// + public string ErrorMessage { get; set; } + + /// + /// Gets or sets the sql task that's executing the operation + /// + public SqlTask SqlTask { get; set; } + + public GenerateScriptOperation(GenerateScriptParams parameters) + { + Validate.IsNotNull(nameof(parameters), parameters); + Parameters = parameters; + } + + /// + /// Execute a task + /// + /// Task execution mode (e.g. script or execute) + /// + /// The method has been called twice in parallel for the same instance. + /// + public void Execute(TaskExecutionMode mode) + { + try + { + var scriptText = GenerateScript(Parameters, cancellation.Token); + if (scriptText != null) + { + SqlTask?.AddScript(SqlTaskStatus.Succeeded, scriptText); + } + } + catch (Exception e) + { + ErrorMessage = e.Message; + Logger.Write(TraceEventType.Error, string.Format( + CultureInfo.InvariantCulture, + "SQL Assessment: generate script operation failed with exception {0}", + e.Message)); + + throw; + } + } + + public void Cancel() + { + cancellation.Cancel(); + } + + #region Helpers + + internal static string GenerateScript(GenerateScriptParams generateScriptParams, + CancellationToken cancellationToken) + { + const string scriptPrologue = + @"IF (NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'AssessmentResult')) +BEGIN + CREATE TABLE [dbo].[AssessmentResult]( + [CheckName] [nvarchar](max) NOT NULL, + [CheckId] [nvarchar](max) NOT NULL, + [RulesetName] [nvarchar](max) NOT NULL, + [RulesetVersion] [nvarchar](max) NOT NULL, + [Severity] [nvarchar](max) NOT NULL, + [Message] [nvarchar](max) NOT NULL, + [TargetPath] [nvarchar](max) NOT NULL, + [TargetType] [nvarchar](max) NOT NULL, + [HelpLink] [nvarchar](max) NOT NULL, + [Timestamp] [datetimeoffset](7) NOT NULL + ) +END +GO +INSERT INTO [dbo].[AssessmentResult] ([CheckName],[CheckId],[RulesetName],[RulesetVersion],[Severity],[Message],[TargetPath],[TargetType],[HelpLink],[Timestamp]) +VALUES"; + + var sb = new StringBuilder(); + if (generateScriptParams.Items != null) + { + sb.Append(scriptPrologue); + foreach (var item in generateScriptParams.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (item.Kind == AssessmentResultItemKind.Note) + { + sb.Append( + $"\r\n('{CUtils.EscapeStringSQuote(item.DisplayName)}','{CUtils.EscapeStringSQuote(item.CheckId)}','{CUtils.EscapeStringSQuote(item.RulesetName)}','{item.RulesetVersion}','{item.Level}','{CUtils.EscapeStringSQuote(item.Message)}','{CUtils.EscapeStringSQuote(item.TargetName)}','{item.TargetType}','{CUtils.EscapeStringSQuote(item.HelpLink)}','{item.Timestamp:yyyy-MM-dd hh:mm:ss.fff zzz}'),"); + } + } + + sb.Length -= 1; + } + + return sb.ToString(); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (!disposed) + { + Cancel(); + cancellation.Dispose(); + disposed = true; + } + } + + #endregion + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/SqlAssessmentService.cs b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/SqlAssessmentService.cs new file mode 100644 index 00000000..99613436 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/SqlAssessment/SqlAssessmentService.cs @@ -0,0 +1,503 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.SqlServer.Management.Assessment; +using Microsoft.SqlServer.Management.Assessment.Configuration; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts; +using Microsoft.SqlTools.ServiceLayer.SqlContext; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.ServiceLayer.Workspace; +using Microsoft.SqlTools.Utility; + +using AssessmentResultItem = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.AssessmentResultItem; +using ConnectionType = Microsoft.SqlTools.ServiceLayer.Connection.ConnectionType; +using InvokeParams = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeParams; +using InvokeRequest = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeRequest; + +namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment +{ + /// + /// Service for running SQL Assessment. + /// + public sealed class SqlAssessmentService : IDisposable + { + private const string ApiVersion = "1.0"; + + #region Singleton Instance Implementation + + private static readonly Lazy LazyInstance + = new Lazy(() => new SqlAssessmentService()); + + internal SqlAssessmentService( + ConnectionService connService, + WorkspaceService workspaceService) + { + ConnectionService = connService; + WorkspaceService = workspaceService; + } + + private SqlAssessmentService() + { + ConnectionService = ConnectionService.Instance; + WorkspaceService = WorkspaceService.Instance; + } + + /// + /// Singleton instance of the query execution service + /// + public static SqlAssessmentService Instance => LazyInstance.Value; + + #endregion + + #region Properties + + /// + /// Gets the used to run assessment operations. + /// + private Engine Engine { get; } = new Engine(); + + /// + /// Gets the instance of the connection service, + /// used to get the connection info for a given owner URI. + /// + private ConnectionService ConnectionService { get; } + + private WorkspaceService WorkspaceService { get; } + + /// + /// Holds a map from the + /// to a that is being ran. + /// + private readonly Lazy> activeRequests = + new Lazy>(() => new ConcurrentDictionary()); + + /// + /// Gets a map from the + /// to a that is being ran. + /// + internal ConcurrentDictionary ActiveRequests => activeRequests.Value; + + #endregion + + /// + /// Initializes the service with the service host, + /// registers request handlers and shutdown event handler. + /// + /// The service host instance to register with + public void InitializeService(ServiceHost serviceHost) + { + // Register handlers for requests + serviceHost.SetRequestHandler(InvokeRequest.Type, HandleInvokeRequest); + serviceHost.SetRequestHandler(GetAssessmentItemsRequest.Type, HandleGetAssessmentItemsRequest); + serviceHost.SetRequestHandler(GenerateScriptRequest.Type, HandleGenerateScriptRequest); + + // Register handler for shutdown event + serviceHost.RegisterShutdownTask((shutdownParams, requestContext) => + { + Dispose(); + return Task.FromResult(0); + }); + } + + #region Request Handlers + + internal Task HandleGetAssessmentItemsRequest( + GetAssessmentItemsParams itemsParams, + RequestContext> requestContext) + { + return this.HandleAssessmentRequest(requestContext, itemsParams, this.GetAssessmentItems); + } + + internal Task HandleInvokeRequest( + InvokeParams invokeParams, + RequestContext> requestContext) + { + return this.HandleAssessmentRequest(requestContext, invokeParams, this.InvokeSqlAssessment); + } + + internal async Task HandleGenerateScriptRequest( + GenerateScriptParams parameters, + RequestContext requestContext) + { + GenerateScriptOperation operation = null; + try + { + operation = new GenerateScriptOperation(parameters); + TaskMetadata metadata = new TaskMetadata + { + TaskOperation = operation, + TaskExecutionMode = parameters.TaskExecutionMode, + ServerName = parameters.TargetServerName, + DatabaseName = parameters.TargetDatabaseName, + Name = SR.SqlAssessmentGenerateScriptTaskName + }; + + var _ = SqlTaskManager.Instance.CreateAndRun(metadata); + + await requestContext.SendResult(new ResultStatus + { + Success = true, + ErrorMessage = operation.ErrorMessage + }); + } + catch (Exception e) + { + Logger.Write(TraceEventType.Error, "SQL Assessment: failed to generate a script. Error: " + e); + await requestContext.SendResult(new ResultStatus() + { + Success = false, + ErrorMessage = operation == null ? e.Message : operation.ErrorMessage, + }); + } + } + + #endregion + + #region Helpers + + private async Task HandleAssessmentRequest( + RequestContext> requestContext, + AssessmentParams requestParams, + Func>> assessmentFunction) + where TResult : AssessmentItemInfo + { + try + { + string randomUri = Guid.NewGuid().ToString(); + + // get connection + if (!ConnectionService.TryFindConnection(requestParams.OwnerUri, out var connInfo)) + { + await requestContext.SendError(SR.SqlAssessmentQueryInvalidOwnerUri); + return; + } + + ConnectParams connectParams = new ConnectParams + { + OwnerUri = randomUri, + Connection = connInfo.ConnectionDetails, + Type = ConnectionType.Default + }; + + if(!connInfo.TryGetConnection(ConnectionType.Default, out var connection)) + { + await requestContext.SendError(SR.SqlAssessmentConnectingError); + } + + var workTask = CallAssessmentEngine( + requestParams, + connectParams, + randomUri, + assessmentFunction) + .ContinueWith(async tsk => + { + await requestContext.SendResult(tsk.Result); + }); + + ActiveRequests.TryAdd(randomUri, workTask); + } + catch (Exception ex) + { + if (ex is StackOverflowException || ex is OutOfMemoryException) + { + throw; + } + + await requestContext.SendError(ex.ToString()); + } + } + + /// + /// This function obtains a live connection, then calls + /// an assessment operation specified by + /// + /// + /// SQL Assessment result item type. + /// + /// + /// Request parameters passed from the host. + /// + /// + /// Connection parameters used to identify and access the target. + /// + /// + /// An URI identifying the request task to enable concurrent execution. + /// + /// + /// A function performing assessment operation for given target. + /// + /// + /// Returns for given target. + /// + internal async Task> CallAssessmentEngine( + AssessmentParams requestParams, + ConnectParams connectParams, + string taskUri, + Func>> assessmentFunc) + where TResult : AssessmentItemInfo + + { + var result = new AssessmentResult + { + ApiVersion = ApiVersion + }; + + await ConnectionService.Connect(connectParams); + + var connection = await ConnectionService.Instance.GetOrOpenConnection(taskUri, ConnectionType.Query); + + try + { + var serverInfo = ReliableConnectionHelper.GetServerVersion(connection); + var hostInfo = ReliableConnectionHelper.GetServerHostInfo(connection); + + var server = new SqlObjectLocator + { + Connection = connection, + EngineEdition = GetEngineEdition(serverInfo.EngineEditionId), + Name = serverInfo.ServerName, + ServerName = serverInfo.ServerName, + Type = SqlObjectType.Server, + Urn = serverInfo.ServerName, + Version = Version.Parse(serverInfo.ServerVersion), + Platform = hostInfo.Platform + }; + + switch (requestParams.TargetType) + { + case SqlObjectType.Server: + Logger.Write( + TraceEventType.Verbose, + $"SQL Assessment: running an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}"); + + result.Items.AddRange(await assessmentFunc(server)); + + Logger.Write( + TraceEventType.Verbose, + $"SQL Assessment: finished an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}"); + break; + case SqlObjectType.Database: + var db = GetDatabaseLocator(server, connection.Database); + Logger.Write( + TraceEventType.Verbose, + $"SQL Assessment: running an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}"); + + result.Items.AddRange(await assessmentFunc(db)); + + Logger.Write( + TraceEventType.Verbose, + $"SQL Assessment: finished an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}"); + break; + } + + result.Success = true; + } + finally + { + ActiveRequests.TryRemove(taskUri, out _); + ConnectionService.Disconnect(new DisconnectParams { OwnerUri = taskUri, Type = null }); + } + + return result; + } + + /// + /// Invokes SQL Assessment and formats results. + /// + /// + /// A sequence of target servers or databases to be assessed. + /// + /// + /// Returns a + /// containing assessment results. + /// + /// + /// Internal for testing + /// + internal async Task> InvokeSqlAssessment(SqlObjectLocator target) + { + var resultsList = await Engine.GetAssessmentResultsList(target); + Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} results."); + + return resultsList.Select(TranslateAssessmentResult).ToList(); + } + + /// + /// Gets the list of checks for given target servers or databases. + /// + /// + /// A sequence of target servers or databases. + /// + /// + /// Returns an + /// containing checks available for given . + /// + internal Task> GetAssessmentItems(SqlObjectLocator target) + { + var result = new List(); + + var resultsList = Engine.GetChecks(target).ToList(); + Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} items."); + + foreach (var r in resultsList) + { + var item = new CheckInfo() + { + CheckId = r.Id, + Description = r.Description, + DisplayName = r.DisplayName, + HelpLink = r.HelpLink, + Level = r.Level.ToString(), + TargetName = $"{target.ServerName}/{target.Name}", + Tags = r.Tags.ToArray(), + TargetType = target.Type, + RulesetName = Engine.Configuration.DefaultRuleset.Name, + RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString() + }; + + result.Add(item); + } + + return Task.FromResult(result); + } + + private AssessmentResultItem TranslateAssessmentResult(IAssessmentResult r) + { + var item = new AssessmentResultItem + { + CheckId = r.Check.Id, + Description = r.Check.Description, + DisplayName = r.Check.DisplayName, + HelpLink = r.Check.HelpLink, + Level = r.Check.Level.ToString(), + Message = r.Message, + TargetName = r.TargetPath, + Tags = r.Check.Tags.ToArray(), + TargetType = r.TargetType, + RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(), + RulesetName = Engine.Configuration.DefaultRuleset.Name, + Timestamp = r.Timestamp + }; + + if (r is IAssessmentNote) + { + item.Kind = AssessmentResultItemKind.Note; + } + else if (r is IAssessmentWarning) + { + item.Kind = AssessmentResultItemKind.Warning; + } + else if (r is IAssessmentError) + { + item.Kind = AssessmentResultItemKind.Error; + } + + return item; + } + + /// + /// Constructs a for specified database. + /// + /// Target server locator. + /// Target database name. + /// Returns a locator for target database. + private static SqlObjectLocator GetDatabaseLocator(SqlObjectLocator server, string databaseName) + { + return new SqlObjectLocator + { + Connection = server.Connection, + EngineEdition = server.EngineEdition, + Name = databaseName, + Platform = server.Platform, + ServerName = server.Name, + Type = SqlObjectType.Database, + Urn = $"{server.Name}/{databaseName}", + Version = server.Version + }; + } + + /// + /// Converts numeric of engine edition + /// returned by SERVERPROPERTY('EngineEdition'). + /// + /// + /// A number returned by SERVERPROPERTY('EngineEdition'). + /// + /// Engine edition is not supported. + /// + /// Returns a + /// corresponding to the . + /// + private static SqlEngineEdition GetEngineEdition(int representation) + { + switch (representation) + { + case 1: return SqlEngineEdition.PersonalOrDesktopEngine; + case 2: return SqlEngineEdition.Standard; + case 3: return SqlEngineEdition.Enterprise; + case 4: return SqlEngineEdition.Express; + case 5: return SqlEngineEdition.AzureDatabase; + case 6: return SqlEngineEdition.DataWarehouse; + case 7: return SqlEngineEdition.StretchDatabase; + case 8: return SqlEngineEdition.ManagedInstance; + default: + throw new ArgumentOutOfRangeException(nameof(representation), + SR.SqlAssessmentUnsuppoertedEdition(representation)); + } + } + + #endregion + + #region IDisposable Implementation + + private bool disposed; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposed) + { + return; + } + + if (disposing) + { + foreach (var request in ActiveRequests) + { + request.Value.Dispose(); + } + + ActiveRequests.Clear(); + } + + disposed = true; + } + + ~SqlAssessmentService() + { + Dispose(false); + } + + #endregion + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlAssessment/SqlAssessmentServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlAssessment/SqlAssessmentServiceTests.cs new file mode 100644 index 00000000..cd6bcd15 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/SqlAssessment/SqlAssessmentServiceTests.cs @@ -0,0 +1,183 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +using Microsoft.SqlServer.Management.Assessment; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using NUnit.Framework; +using Xunit; +using Assert = Xunit.Assert; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.SqlAssessment +{ + public class SqlAssessmentServiceTests + { + private delegate Task> AssessmentMethod(SqlObjectLocator locator); + + private static readonly string[] AllowedSeverityLevels = { "Information", "Warning", "Critical" }; + + [Fact] + public async void GetAssessmentItemsServerTest() + { + var response = await CallAssessment( + nameof(SqlAssessmentService.GetAssessmentItems), + SqlObjectType.Server); + + Assert.All( + response.Items, + i => + { + AssertInfoPresent(i); + }); + } + + [Fact] + public async void InvokeSqlAssessmentServerTest() + { + var response = await CallAssessment( + nameof(SqlAssessmentService.InvokeSqlAssessment), + SqlObjectType.Server); + + + Assert.All( + response.Items, + i => + { + Assert.NotNull(i.Message); + Assert.NotEmpty(i.Message); + + if (i.Kind == 0) + { + AssertInfoPresent(i); + } + }); + } + + [Fact] + public async void GetAssessmentItemsDatabaseTest() + { + const string DatabaseName = "tempdb"; + var response = await CallAssessment( + nameof(SqlAssessmentService.GetAssessmentItems), + SqlObjectType.Database, + DatabaseName); + + Assert.All( + response.Items, + i => + { + StringAssert.EndsWith("/" + DatabaseName, i.TargetName); + AssertInfoPresent(i); + }); + } + + [Fact] + public async void InvokeSqlAssessmentIDatabaseTest() + { + const string DatabaseName = "tempdb"; + var response = await CallAssessment( + nameof(SqlAssessmentService.InvokeSqlAssessment), + SqlObjectType.Database, + DatabaseName); + + Assert.All( + response.Items, + i => + { + StringAssert.EndsWith("/" + DatabaseName, i.TargetName); + Assert.NotNull(i.Message); + Assert.NotEmpty(i.Message); + + if (i.Kind == 0) + { + AssertInfoPresent(i); + } + }); + } + + private static async Task> CallAssessment( + string methodName, + SqlObjectType sqlObjectType, + string databaseName = "master") + where TResult : AssessmentItemInfo + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); + var connInfo = liveConnection.ConnectionInfo; + + AssessmentResult response; + + using (var service = new SqlAssessmentService( + TestServiceProvider.Instance.ConnectionService, + TestServiceProvider.Instance.WorkspaceService)) + { + string randomUri = Guid.NewGuid().ToString(); + AssessmentParams requestParams = + new AssessmentParams { OwnerUri = randomUri, TargetType = sqlObjectType }; + ConnectParams connectParams = new ConnectParams + { + OwnerUri = requestParams.OwnerUri, + Connection = connInfo.ConnectionDetails, + Type = ConnectionType.Default + }; + + var methodInfo = typeof(SqlAssessmentService).GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.NotNull(methodInfo); + + var func = (AssessmentMethod)Delegate.CreateDelegate( + typeof(AssessmentMethod), + service, + methodInfo); + + response = await service.CallAssessmentEngine( + requestParams, + connectParams, + randomUri, + t => func(t)); + } + + Assert.NotNull(response); + if (response.Success) + { + Assert.All( + response.Items, + i => + { + Assert.Equal(sqlObjectType, i.TargetType); + Assert.Contains(i.Level, AllowedSeverityLevels); + }); + } + + return response; + } + + private void AssertInfoPresent(AssessmentItemInfo item) + { + Assert.NotNull(item.CheckId); + Assert.NotEmpty(item.CheckId); + Assert.NotNull(item.DisplayName); + Assert.NotEmpty(item.DisplayName); + Assert.NotNull(item.Description); + Assert.NotEmpty(item.Description); + Assert.NotNull(item.Tags); + Assert.All(item.Tags, t => + { + Assert.NotNull(t); + Assert.NotEmpty(t); + }); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlAssessment/GenerateScriptOperationTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlAssessment/GenerateScriptOperationTests.cs new file mode 100644 index 00000000..77ab90f3 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/SqlAssessment/GenerateScriptOperationTests.cs @@ -0,0 +1,159 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Assessment; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment; +using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts; +using Microsoft.SqlTools.ServiceLayer.TaskServices; +using Moq; +using NUnit.Framework; +using Xunit; +using Assert = Xunit.Assert; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.SqlAssessment +{ + public class GenerateScriptOperationTests + { + private static readonly GenerateScriptParams SampleParams = new GenerateScriptParams + { + Items = new List + { + new AssessmentResultItem + { + CheckId = "C1", + Description = "Desc1", + DisplayName = "DN1", + HelpLink = "HL1", + Kind = AssessmentResultItemKind.Note, + Level = "Information", + Message = "Msg'1", + TargetName = "proj[*]_dev", + TargetType = SqlObjectType.Server, + Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.Zero), + RulesetName = "Microsoft Ruleset", + RulesetVersion = "1.3" + }, + new AssessmentResultItem + { + CheckId = "C-2", + Description = "Desc2", + DisplayName = "D N2", + HelpLink = "http://HL2", + Kind = AssessmentResultItemKind.Warning, + Level = "Warning", + Message = "Msg'1", + TargetName = "proj[*]_devW", + TargetType = SqlObjectType.Database, + Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromHours(3)), + RulesetName = "Microsoft Ruleset", + RulesetVersion = "1.3" + }, + new AssessmentResultItem + { + CheckId = "C'3", + Description = "Des'c3", + DisplayName = "D'N1", + HelpLink = "HL'1", + Kind = AssessmentResultItemKind.Error, + Level = "Critical", + Message = "Msg'1", + TargetName = "proj[*]_devE", + TargetType = SqlObjectType.Server, + Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromMinutes(-90)), + RulesetName = "Microsoft Ruleset", + RulesetVersion = "1.3" + }, + new AssessmentResultItem + { + CheckId = "C-2", + Description = "Desc2", + DisplayName = "D N2", + HelpLink = "http://HL2", + Kind = AssessmentResultItemKind.Note, + Level = "Warning", + Message = "Msg'1", + TargetName = "proj[*]_dev", + TargetType = SqlObjectType.Database, + Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromHours(3)), + RulesetName = "Microsoft Ruleset", + RulesetVersion = "1.3" + }, + new AssessmentResultItem + { + CheckId = "C'3", + Description = "Des'c3", + DisplayName = "D'N1", + HelpLink = "HL'1", + Kind = AssessmentResultItemKind.Note, + Level = "Critical", + Message = "Msg'1", + TargetName = "proj[*]_dev", + TargetType = SqlObjectType.Server, + Timestamp = new DateTimeOffset(2001, 5, 25, 13, 42, 00, TimeSpan.FromMinutes(-90)), + RulesetName = "Microsoft Ruleset", + RulesetVersion = "1.3" + } + } + }; + + private const string SampleScript = + @"IF (NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'AssessmentResult')) +BEGIN + CREATE TABLE [dbo].[AssessmentResult]( + [CheckName] [nvarchar](max) NOT NULL, + [CheckId] [nvarchar](max) NOT NULL, + [RulesetName] [nvarchar](max) NOT NULL, + [RulesetVersion] [nvarchar](max) NOT NULL, + [Severity] [nvarchar](max) NOT NULL, + [Message] [nvarchar](max) NOT NULL, + [TargetPath] [nvarchar](max) NOT NULL, + [TargetType] [nvarchar](max) NOT NULL, + [HelpLink] [nvarchar](max) NOT NULL, + [Timestamp] [datetimeoffset](7) NOT NULL + ) +END +GO +INSERT INTO [dbo].[AssessmentResult] ([CheckName],[CheckId],[RulesetName],[RulesetVersion],[Severity],[Message],[TargetPath],[TargetType],[HelpLink],[Timestamp]) +VALUES +('DN1','C1','Microsoft Ruleset','1.3','Information','Msg''1','proj[*]_dev','Server','HL1','2001-05-25 01:42:00.000 +00:00'), +('D N2','C-2','Microsoft Ruleset','1.3','Warning','Msg''1','proj[*]_dev','Database','http://HL2','2001-05-25 01:42:00.000 +03:00'), +('D''N1','C''3','Microsoft Ruleset','1.3','Critical','Msg''1','proj[*]_dev','Server','HL''1','2001-05-25 01:42:00.000 -01:30')"; + + [Fact] + public void GenerateScriptTest() + { + var scriptText = GenerateScriptOperation.GenerateScript(SampleParams, CancellationToken.None); + Assert.Equal(SampleScript, scriptText); + } + + [Fact] + public void ExecuteTest() + { + var subject = new GenerateScriptOperation(SampleParams); + var taskMetadata = new TaskMetadata(); + using (var sqlTask = new SqlTask(taskMetadata, DummyOpFunction, DummyOpFunction)) + { + subject.SqlTask = sqlTask; + sqlTask.ScriptAdded += ValidateScriptAdded; + subject.Execute(TaskExecutionMode.Script); + } + } + + private void ValidateScriptAdded(object sender, TaskEventArgs e) + { + Assert.Equal(SqlTaskStatus.Succeeded, e.TaskData.Status); + Assert.Equal(SampleScript, e.TaskData.Script); + } + + private static Task DummyOpFunction(SqlTask _) + { + return Task.FromResult(new TaskResult() {TaskStatus = SqlTaskStatus.Succeeded}); + } + } +}