diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 8f4a4cb5..dd7cbc2c 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -2397,6 +2397,30 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string StoredProcedureScriptParameterComment + { + get + { + return Keys.GetString(Keys.StoredProcedureScriptParameterComment); + } + } + + public static string ScriptingGeneralError + { + get + { + return Keys.GetString(Keys.ScriptingGeneralError); + } + } + + public static string ScriptingExecuteNotSupportedError + { + get + { + return Keys.GetString(Keys.ScriptingExecuteNotSupportedError); + } + } + public static string unavailable { get @@ -4597,6 +4621,15 @@ namespace Microsoft.SqlTools.ServiceLayer public const string ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid = "ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid"; + public const string StoredProcedureScriptParameterComment = "StoredProcedureScriptParameterComment"; + + + public const string ScriptingGeneralError = "ScriptingGeneralError"; + + + public const string ScriptingExecuteNotSupportedError = "ScriptingExecuteNotSupportedError"; + + public const string unavailable = "unavailable"; diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index e029cd84..ac70645e 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1381,6 +1381,18 @@ Error parsing ScriptingListObjectsCompleteParams.ConnectionString property. + + -- TODO: Set parameter values here. + + + + An error occurred while scripting the objects. + + + + Scripting as Execute is only supported for Stored Procedures + + Unavailable diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 38ee618f..35c56f15 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -691,7 +691,10 @@ ScriptingParams_FilePath_Property_Invalid = Invalid directory specified by the S ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid = Error parsing ScriptingListObjectsCompleteParams.ConnectionString property. +StoredProcedureScriptParameterComment = -- TODO: Set parameter values here. +ScriptingGeneralError = An error occurred while scripting the objects. +ScriptingExecuteNotSupportedError = Scripting as Execute is only supported for Stored Procedures ############################################################################ # Admin Service diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index 40acd1b9..a26431ec 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2312,6 +2312,21 @@ . Parameters: 0 - value (string), 1 - columnType (string) + + -- TODO: Set parameter values here. + -- TODO: Set parameter values here. + + + + An error occurred while scripting the objects. + An error occurred while scripting the objects + + + + Scripting as Execute is only supported for Stored Procedures + Scripting as Execute is only supported for Stored Procedures + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptAsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptAsRequest.cs deleted file mode 100644 index 2f4fd891..00000000 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptAsRequest.cs +++ /dev/null @@ -1,20 +0,0 @@ -// -// 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.Scripting.Contracts -{ - - /// - /// Script as request message type - /// - public class ScriptingScriptAsRequest - { - public static readonly - RequestType Type = - RequestType.Create("scripting/scriptas"); - } -} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOperationType.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOperationType.cs new file mode 100644 index 00000000..c016e0bb --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOperationType.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts +{ + /// + /// Scripting Operation type + /// + public enum ScriptingOperationType + { + Select = 0, + Create = 1, + Insert = 2, + Update = 3, + Delete = 4, + Execute = 5 + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs index 2fcf7074..4e058f65 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs @@ -101,7 +101,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// ScriptCreate /// ScriptDrop /// ScriptCreateDrop - /// ScriptSelect /// /// /// The default is ScriptCreate. diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingRequest.cs index fc819e25..d853c1ef 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingRequest.cs @@ -73,6 +73,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public string OwnerUri { get; set; } + /// + /// The script operation + /// + public ScriptingOperationType Operation { get; set; } = ScriptingOperationType.Create; + } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs index e78b6d32..9ec1c74a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs @@ -15,6 +15,7 @@ using System.Text; using System.Globalization; using Microsoft.SqlServer.Management.SqlScriptPublish; using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlServer.Management.Sdk.Sfc; namespace Microsoft.SqlTools.ServiceLayer.Scripting { @@ -24,21 +25,37 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting public class ScriptAsScriptingOperation : SmoScriptingOperation { private static Dictionary scriptCompatabilityMap = LoadScriptCompatabilityMap(); + /// + /// Left delimiter for an named object + /// + public const char LeftDelimiter = '['; + + /// + /// right delimiter for a named object + /// + public const char RightDelimiter = ']'; public ScriptAsScriptingOperation(ScriptingParams parameters, ServerConnection serverConnection): base(parameters) { + Validate.IsNotNull("serverConnection", serverConnection); ServerConnection = serverConnection; } public ScriptAsScriptingOperation(ScriptingParams parameters) : base(parameters) { + SqlConnection sqlConnection = new SqlConnection(this.Parameters.ConnectionString); + ServerConnection = new ServerConnection(sqlConnection); + disconnectAtDispose = true; } internal ServerConnection ServerConnection { get; set; } + private string serverName; + private string databaseName; + private bool disconnectAtDispose = false; + public override void Execute() { - SqlServer.Management.Smo.Scripter scripter = null; try { this.CancellationToken.ThrowIfCancellationRequested(); @@ -54,24 +71,32 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { ServerConnection.Connect(); } - scripter = new SqlServer.Management.Smo.Scripter(server); + + UrnCollection urns = CreateUrns(ServerConnection); ScriptingOptions options = new ScriptingOptions(); SetScriptBehavior(options); - ScriptAsOptions scriptAsOptions = new ScriptAsOptions(this.Parameters.ScriptOptions); - PopulateAdvancedScriptOptions(scriptAsOptions, options); + PopulateAdvancedScriptOptions(this.Parameters.ScriptOptions, options); options.WithDependencies = false; + // TODO: Not including the header by default. We have to get this option from client + options.IncludeHeaders = false; + + // Scripting data is not avaialable in the scripter options.ScriptData = false; SetScriptingOptions(options); - // TODO: Not including the header by default. We have to get this option from client - options.IncludeHeaders = false; - scripter.Options = options; - scripter.Options.ScriptData = false; - scripter.ScriptingError += ScripterScriptingError; - UrnCollection urns = CreateUrns(ServerConnection); - var result = scripter.Script(urns); - resultScript = GetScript(options, result); - + switch (this.Parameters.Operation) + { + case ScriptingOperationType.Create: + case ScriptingOperationType.Delete: // Using Delete here is wrong. delete usually means delete rows from table but sqlopsstudio sending the operation name as delete instead of drop + resultScript = GenerateScriptAs(server, urns, options); + break; + case ScriptingOperationType.Select: + resultScript = GenerateScriptSelect(server, urns); + break; + case ScriptingOperationType.Execute: + resultScript = GenerareScriptAsExecute(server, urns, options); + break; + } this.CancellationToken.ThrowIfCancellationRequested(); @@ -88,6 +113,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { Success = true, }); + + this.SendPlanNotificationEvent(new ScriptingPlanNotificationParams + { + ScriptingObjects = this.Parameters.ScriptingObjects, + Count = 1, + }); } catch (Exception e) { @@ -106,18 +137,326 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { OperationId = OperationId, HasError = true, - ErrorMessage = e.Message, + ErrorMessage = $"{SR.ScriptingGeneralError} {e.Message}", ErrorDetails = e.ToString(), }); } } finally + { + if (disconnectAtDispose && ServerConnection != null && ServerConnection.IsOpen) + { + ServerConnection.Disconnect(); + } + } + } + + private string GenerateScriptSelect(Server server, UrnCollection urns) + { + string script = string.Empty; + ScriptingObject scriptingObject = this.Parameters.ScriptingObjects[0]; + Urn objectUrn = urns[0]; + string typeName = objectUrn.GetNameForType(scriptingObject.Type); + + // select from service broker + if (string.Compare(typeName, "ServiceBroker", StringComparison.CurrentCultureIgnoreCase) == 0) + { + script = Scripter.SelectAllValuesFromTransmissionQueue(objectUrn); + } + + // select from queues + else if (string.Compare(typeName, "Queues", StringComparison.CurrentCultureIgnoreCase) == 0 || + string.Compare(typeName, "SystemQueues", StringComparison.CurrentCultureIgnoreCase) == 0) + { + script = Scripter.SelectAllValues(objectUrn); + } + + // select from table or view + else + { + Database db = server.Databases[databaseName]; + bool isDw = db.IsSqlDw; + script = new Scripter().SelectFromTableOrView(server, objectUrn, isDw); + } + + return script; + } + + private string GenerareScriptAsExecute(Server server, UrnCollection urns, ScriptingOptions options) + { + string script = string.Empty; + ScriptingObject scriptingObject = this.Parameters.ScriptingObjects[0]; + Urn urn = urns[0]; + + // get the object + StoredProcedure sp = server.GetSmoObject(urn) as StoredProcedure; + + Database parentObject = server.GetSmoObject(urn.Parent) as Database; + + StringBuilder executeStatement = new StringBuilder(); + + // list of DECLARE + StringBuilder declares = new StringBuilder(); + // Parameters to be passed + StringBuilder parameterList = new StringBuilder(); + if (sp == null || parentObject == null) + { + throw new InvalidOperationException(SR.ScriptingExecuteNotSupportedError); + } + WriteUseDatabase(parentObject, executeStatement, options); + + // character string to put in front of each parameter. First one is just carriage return + // the rest will have a "," in front as well. + string newLine = Environment.NewLine; + string paramListPreChar = $"{newLine} "; + for (int i = 0; i < sp.Parameters.Count; i++) + { + StoredProcedureParameter spp = sp.Parameters[i]; + + declares.AppendFormat("DECLARE {0} {1}{2}" + , QuoteObjectName(spp.Name) + , GetDatatype(spp.DataType, options) + , newLine); + + parameterList.AppendFormat("{0}{1}" + , paramListPreChar + , QuoteObjectName(spp.Name)); + + // if this is the first time through change the prefix to include a "," + if (i == 0) + { + paramListPreChar = $"{newLine} ,"; + } + + // mark any output parameters as such. + if (spp.IsOutputParameter) + { + parameterList.Append(" OUTPUT"); + } + } + + // build the execute statement + if (sp.ImplementationType == ImplementationType.TransactSql) + { + executeStatement.Append("EXECUTE @RC = "); + } + else + { + executeStatement.Append("EXECUTE "); + } + + // get the object name + executeStatement.Append(GenerateSchemaQualifiedName(sp.Schema, sp.Name, options.SchemaQualify)); + + string formatString = sp.ImplementationType == ImplementationType.TransactSql + ? "DECLARE @RC int{5}{0}{5}{1}{5}{5}{2} {3}{5}{4}" + : "{0}{5}{1}{5}{5}{2} {3}{5}{4}"; + + script = string.Format(CultureInfo.InvariantCulture, formatString, + declares, + SR.StoredProcedureScriptParameterComment, + executeStatement, + parameterList, + CommonConstants.DefaultBatchSeperator, + newLine); + + return script; + } + + /// + /// Generate a schema qualified name (e.g. [schema].[objectName]) for an object if the option for SchemaQualify is true + /// + /// The schema name. May be null or empty in which case it will be ignored + /// The object name. + /// Whether to schema qualify the object or not + /// The object name, quoted as appropriate and schema-qualified if the option is set + static private string GenerateSchemaQualifiedName(string schema, string objectName, bool schemaQualify) + { + var qualifiedName = new StringBuilder(); + + if (schemaQualify && !String.IsNullOrEmpty(schema)) + { + // schema.name + qualifiedName.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}", GetDelimitedString(schema), GetDelimitedString(objectName)); + } + else + { + // name + qualifiedName.AppendFormat(CultureInfo.InvariantCulture, "{0}", GetDelimitedString(objectName)); + } + + return qualifiedName.ToString(); + } + + /// + /// getting delimited string + /// + /// string + /// string + static private string GetDelimitedString(string str) + { + if (string.IsNullOrEmpty(str)) + { + return String.Empty; + } + else + { + StringBuilder qualifiedName = new StringBuilder(); + qualifiedName.AppendFormat("{0}{1}{2}", + LeftDelimiter, + QuoteObjectName(str), + RightDelimiter); + return qualifiedName.ToString(); + } + } + + /// + /// turn a smo datatype object into a type that can be inserted into tsql, e.g. nvarchar(20) + /// + /// + /// + /// + internal static string GetDatatype(DataType type, ScriptingOptions options) + { + // string we'll return. + string rv = string.Empty; + + string dataType = type.Name; + switch (type.SqlDataType) + { + // char, nchar, nchar, nvarchar, varbinary, nvarbinary are all displayed as type(length) + // length of -1 is taken to be type(max). max isn't localizable. + case SqlDataType.Char: + case SqlDataType.NChar: + case SqlDataType.VarChar: + case SqlDataType.NVarChar: + case SqlDataType.Binary: + case SqlDataType.VarBinary: + rv = string.Format(CultureInfo.InvariantCulture, + "{0}({1})", + dataType, + type.MaximumLength); + break; + case SqlDataType.VarCharMax: + case SqlDataType.NVarCharMax: + case SqlDataType.VarBinaryMax: + rv = string.Format(CultureInfo.InvariantCulture, + "{0}(max)", + dataType); + break; + // numeric and decimal are displayed as type precision,scale + case SqlDataType.Numeric: + case SqlDataType.Decimal: + rv = string.Format(CultureInfo.InvariantCulture, + "{0}({1},{2})", + dataType, + type.NumericPrecision, + type.NumericScale); + break; + //time, datetimeoffset and datetime2 are displayed as type scale + case SqlDataType.Time: + case SqlDataType.DateTimeOffset: + case SqlDataType.DateTime2: + rv = string.Format(CultureInfo.InvariantCulture, + "{0}({1})", + dataType, + type.NumericScale); + break; + // anything else is just type. + case SqlDataType.Xml: + if (type.Schema != null && type.Schema.Length > 0 && dataType != null && dataType.Length > 0) + { + rv = String.Format(CultureInfo.InvariantCulture + , "xml ({0}{2}{1}.{0}{3}{1})" + , LeftDelimiter + , RightDelimiter + , QuoteObjectName(type.Schema) + , QuoteObjectName(dataType)); + } + else + { + rv = "xml"; + } + break; + case SqlDataType.UserDefinedDataType: + case SqlDataType.UserDefinedTableType: + case SqlDataType.UserDefinedType: + //User defined types may be in a non-DBO schema so append it if necessary + rv = GenerateSchemaQualifiedName(type.Schema, dataType, options.SchemaQualify); + break; + default: + rv = dataType; + break; + + } + return rv; + } + + /// + /// Double quotes certain characters in object name + /// + /// + public static string QuoteObjectName(string sqlObject) + { + + int len = sqlObject.Length; + StringBuilder result = new StringBuilder(sqlObject.Length); + for (int i = 0; i < len; i++) + { + if (sqlObject[i] == ']') + { + result.Append(']'); + } + result.Append(sqlObject[i]); + } + + return result.ToString(); + } + + private static void WriteUseDatabase(Database parentObject, StringBuilder stringBuilder , ScriptingOptions options) + { + if (options.IncludeDatabaseContext) + { + string useDb = string.Format(CultureInfo.InvariantCulture, "USE {0}", CommonConstants.DefaultBatchSeperator); + if (!options.NoCommandTerminator) + { + stringBuilder.Append(useDb); + + } + else + { + stringBuilder.Append(useDb); + stringBuilder.Append(Environment.NewLine); + } + } + } + + private string GenerateScriptAs(Server server, UrnCollection urns, ScriptingOptions options) + { + SqlServer.Management.Smo.Scripter scripter = null; + string resultScript = string.Empty; + try + { + scripter = new SqlServer.Management.Smo.Scripter(server); + + scripter.Options = options; + scripter.ScriptingError += ScripterScriptingError; + var result = scripter.Script(urns); + resultScript = GetScript(options, result); + } + catch + { + throw; + } + finally { if (scripter != null) { scripter.ScriptingError -= this.ScripterScriptingError; } } + + return resultScript; } private string GetScript(ScriptingOptions options, StringCollection stringCollection) @@ -150,8 +489,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { IEnumerable selectedObjects = new List(this.Parameters.ScriptingObjects); - string server = serverConnection.TrueName; - string database = new SqlConnectionStringBuilder(this.Parameters.ConnectionString).InitialCatalog; + serverName = serverConnection.TrueName; + databaseName = new SqlConnectionStringBuilder(this.Parameters.ConnectionString).InitialCatalog; UrnCollection urnCollection = new UrnCollection(); foreach (var scriptingObject in selectedObjects) { @@ -160,7 +499,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting // TODO: get the default schema scriptingObject.Schema = "dbo"; } - urnCollection.Add(scriptingObject.ToUrn(server, database)); + urnCollection.Add(scriptingObject.ToUrn(serverName, databaseName)); } return urnCollection; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs index a9d1bf65..71f323bd 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs @@ -29,11 +29,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { } - /// - /// Event raised when a scripting operation has resolved which database objects will be scripted. - /// - public event EventHandler PlanNotification; - public override void Execute() { SqlScriptPublishModel publishModel = null; @@ -118,25 +113,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting protected override void SendCompletionNotificationEvent(ScriptingCompleteParams parameters) { - this.SetCommonEventProperties(parameters); base.SendCompletionNotificationEvent(parameters); } - private void SendPlanNotificationEvent(ScriptingPlanNotificationParams parameters) + protected override void SendPlanNotificationEvent(ScriptingPlanNotificationParams parameters) { - this.SetCommonEventProperties(parameters); - this.PlanNotification?.Invoke(this, parameters); + base.SendPlanNotificationEvent(parameters); } protected override void SendProgressNotificationEvent(ScriptingProgressNotificationParams parameters) { - this.SetCommonEventProperties(parameters); base.SendProgressNotificationEvent(parameters); } - private void SetCommonEventProperties(ScriptingEventParams parameters) + protected override void SetCommonEventProperties(ScriptingEventParams parameters) { - parameters.OperationId = this.OperationId; + base.SetCommonEventProperties(parameters); parameters.SequenceNumber = this.eventSequenceNumber; this.eventSequenceNumber += 1; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs index 5db281a8..127ab1cf 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs @@ -76,7 +76,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting /// public void InitializeService(ServiceHost serviceHost) { - serviceHost.SetRequestHandler(ScriptingScriptAsRequest.Type, HandleScriptingScriptAsRequest); serviceHost.SetRequestHandler(ScriptingRequest.Type, this.HandleScriptExecuteRequest); serviceHost.SetRequestHandler(ScriptingCancelRequest.Type, this.HandleScriptCancelRequest); serviceHost.SetRequestHandler(ScriptingListObjectsRequest.Type, this.HandleListObjectsRequest); @@ -112,81 +111,17 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting /// /// Handles request to start the scripting operation /// - public async Task HandleScriptingScriptAsRequest(ScriptingParams parameters, RequestContext requestContext) + public async Task HandleScriptExecuteRequest(ScriptingParams parameters, RequestContext requestContext) { + SmoScriptingOperation operation = null; + try { // if a connection string wasn't provided as a parameter then // use the owner uri property to lookup its associated ConnectionInfo // and then build a connection string out of that ConnectionInfo connInfo = null; - if (parameters.ConnectionString == null || parameters.ScriptOptions.ScriptCreateDrop == "ScriptSelect") - { - ScriptingService.ConnectionServiceInstance.TryFindConnection(parameters.OwnerUri, out connInfo); - if (connInfo != null) - { - connInfo.ConnectionDetails.PersistSecurityInfo = true; - parameters.ConnectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); - } - else - { - throw new Exception("Could not find ConnectionInfo"); - } - } - - // if the scripting operation is for SELECT then handle that message differently - // for SELECT we'll build the SQL directly whereas other scripting operations depend on SMO - if (parameters.ScriptOptions.ScriptCreateDrop == "ScriptSelect") - { - RunSelectTask(connInfo, parameters, requestContext); - } - else - { - RunScriptAsTask(connInfo, parameters, requestContext); - } - } - catch (Exception e) - { - await requestContext.SendError(e); - } - } - - private void RunScriptAsTask(ConnectionInfo connInfo, ScriptingParams parameters, RequestContext requestContext) - { - ScriptAsScriptingOperation operation = new ScriptAsScriptingOperation(parameters); - ConnectionServiceInstance.ConnectionQueue.QueueBindingOperation( - key: ConnectionServiceInstance.ConnectionQueue.AddConnectionContext(connInfo, "Scripting"), - bindingTimeout: ScriptingOperationTimeout, - bindOperation: (bindingContext, cancelToken) => - { - string script = string.Empty; - operation.ServerConnection = bindingContext.ServerConnection; - operation.ProgressNotification += (sender, e) => requestContext.SendEvent(ScriptingProgressNotificationEvent.Type, e); - operation.CompleteNotification += (sender, e) => this.SendScriptingCompleteEvent(requestContext, ScriptingCompleteEvent.Type, e, operation, parameters.ScriptDestination); - - RunTask(requestContext, operation); - - return null; - }, - timeoutOperation: (bindingContext) => - { - this.SendScriptingCompleteEvent(requestContext, ScriptingCompleteEvent.Type, new ScriptingCompleteParams { Success = false }, operation, parameters.ScriptDestination); - return null; - }); - } - - /// - /// Handles request to start the scripting operation - /// - public async Task HandleScriptExecuteRequest(ScriptingParams parameters, RequestContext requestContext) - { - try - { - // if a connection string wasn't provided as a parameter then - // use the owner uri property to lookup its associated ConnectionInfo - // and then build a connection string out of that - ConnectionInfo connInfo = null; - if (parameters.ConnectionString == null || parameters.ScriptOptions.ScriptCreateDrop == "ScriptSelect") + if (parameters.ConnectionString == null) { ScriptingService.ConnectionServiceInstance.TryFindConnection(parameters.OwnerUri, out connInfo); if (connInfo != null) @@ -198,22 +133,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting throw new Exception("Could not find ConnectionInfo"); } } - - // if the scripting operation is for SELECT then handle that message differently - // for SELECT we'll build the SQL directly whereas other scripting operations depend on SMO - if (parameters.ScriptOptions.ScriptCreateDrop == "ScriptSelect") + + if (!ShouldCreateScriptAsOperation(parameters)) { - RunSelectTask(connInfo, parameters, requestContext); + operation = new ScriptingScriptOperation(parameters); } else { - ScriptingScriptOperation operation = new ScriptingScriptOperation(parameters); - operation.PlanNotification += (sender, e) => requestContext.SendEvent(ScriptingPlanNotificationEvent.Type, e).Wait(); - operation.ProgressNotification += (sender, e) => requestContext.SendEvent(ScriptingProgressNotificationEvent.Type, e).Wait(); - operation.CompleteNotification += (sender, e) => this.SendScriptingCompleteEvent(requestContext, ScriptingCompleteEvent.Type, e, operation, parameters.ScriptDestination); - - RunTask(requestContext, operation); + operation = new ScriptAsScriptingOperation(parameters); } + + operation.PlanNotification += (sender, e) => requestContext.SendEvent(ScriptingPlanNotificationEvent.Type, e).Wait(); + operation.ProgressNotification += (sender, e) => requestContext.SendEvent(ScriptingProgressNotificationEvent.Type, e).Wait(); + operation.CompleteNotification += (sender, e) => this.SendScriptingCompleteEvent(requestContext, ScriptingCompleteEvent.Type, e, operation, parameters.ScriptDestination); + + RunTask(requestContext, operation); + } catch (Exception e) { @@ -221,6 +156,23 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } } + private bool ShouldCreateScriptAsOperation(ScriptingParams parameters) + { + // Scripting as operation should be used to script one object. + // Scripting data and scripting to file is not supported by scripting as operation + // To script Select, alter and execute use scripting as operation. The other operation doesn't support those types + if( (parameters.ScriptingObjects != null && parameters.ScriptingObjects.Count == 1 && parameters.ScriptOptions != null + && parameters.ScriptOptions.TypeOfDataToScript == "SchemaOnly" && parameters.ScriptDestination == "ToEditor") || + parameters.Operation == ScriptingOperationType.Select || parameters.Operation == ScriptingOperationType.Execute) + { + return true; + } + else + { + return false; + } + } + /// /// Handles request to cancel a script operation. /// @@ -264,87 +216,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } } - private Urn BuildScriptingObjectUrn( - Server server, - SqlConnectionStringBuilder connectionStringBuilder, - ScriptingObject scriptingObject) - { - string serverName = server.Name.ToUpper(); - - // remove the port from server name if specified - int commaPos = serverName.IndexOf(','); - if (commaPos >= 0) - { - serverName = serverName.Substring(0, commaPos); - } - - // build the URN - string urnString = string.Format( - "Server[@Name='{0}']/Database[@Name='{1}']/{2}[@Name='{3}' {4}]", - serverName, - connectionStringBuilder.InitialCatalog, - scriptingObject.Type, - scriptingObject.Name, - scriptingObject.Schema != null ? string.Format("and @Schema = '{0}'", scriptingObject.Schema) : string.Empty); - - return new Urn(urnString); - } - - /// - /// Runs the async task that performs the scripting operation. - /// - private void RunSelectTask(ConnectionInfo connInfo, ScriptingParams parameters, RequestContext requestContext) - { - ConnectionServiceInstance.ConnectionQueue.QueueBindingOperation( - key: ConnectionServiceInstance.ConnectionQueue.AddConnectionContext(connInfo, "Scripting"), - bindingTimeout: ScriptingOperationTimeout, - bindOperation: (bindingContext, cancelToken) => - { - string script = string.Empty; - ScriptingObject scriptingObject = parameters.ScriptingObjects[0]; - try - { - Server server = new Server(bindingContext.ServerConnection); - server.DefaultTextMode = true; - - // build object URN - SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder(parameters.ConnectionString); - Urn objectUrn = BuildScriptingObjectUrn(server, connectionStringBuilder, scriptingObject); - string typeName = objectUrn.GetNameForType(scriptingObject.Type); - - // select from service broker - if (string.Compare(typeName, "ServiceBroker", StringComparison.CurrentCultureIgnoreCase) == 0) - { - script = Scripter.SelectAllValuesFromTransmissionQueue(objectUrn); - } - - // select from queues - else if (string.Compare(typeName, "Queues", StringComparison.CurrentCultureIgnoreCase) == 0 || - string.Compare(typeName, "SystemQueues", StringComparison.CurrentCultureIgnoreCase) == 0) - { - script = Scripter.SelectAllValues(objectUrn); - } - - // select from table or view - else - { - Database db = server.Databases[connectionStringBuilder.InitialCatalog]; - bool isDw = db.IsSqlDw; - script = new Scripter().SelectFromTableOrView(server, objectUrn, isDw); - } - - // send script result to client - requestContext.SendResult(new ScriptingResult { Script = script }).Wait(); - } - catch (Exception e) - { - requestContext.SendError(e).Wait(); - } - - return null; - }); - } - /// /// Runs the async task that performs the scripting operation. /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs index 5dd9afb8..9ce935ea 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs @@ -42,16 +42,34 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting /// public event EventHandler ProgressNotification; + /// + /// Event raised when a scripting operation has resolved which database objects will be scripted. + /// + public event EventHandler PlanNotification; + protected virtual void SendCompletionNotificationEvent(ScriptingCompleteParams parameters) { + this.SetCommonEventProperties(parameters); this.CompleteNotification?.Invoke(this, parameters); } protected virtual void SendProgressNotificationEvent(ScriptingProgressNotificationParams parameters) { + this.SetCommonEventProperties(parameters); this.ProgressNotification?.Invoke(this, parameters); } + protected virtual void SendPlanNotificationEvent(ScriptingPlanNotificationParams parameters) + { + this.SetCommonEventProperties(parameters); + this.PlanNotification?.Invoke(this, parameters); + } + + protected virtual void SetCommonEventProperties(ScriptingEventParams parameters) + { + parameters.OperationId = this.OperationId; + } + protected string GetServerNameFromLiveInstance(string connectionString) { string serverName = null; diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs index 364b2d63..96daba75 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs @@ -89,7 +89,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting public async void VerifyScriptAsCreateTable() { string query = "CREATE TABLE testTable1 (c1 int)"; - string scriptCreateDrop = "ScriptCreate"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Create; ScriptingObject scriptingObject = new ScriptingObject { Name = "testTable1", @@ -101,11 +101,67 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); } + [Fact] + public async void VerifyScriptAsExecuteTableFailes() + { + string query = "CREATE TABLE testTable1 (c1 int)"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Execute; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testTable1", + Schema = "dbo", + Type = "Table" + }; + string expectedScript = null; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + [Fact] + public async void VerifyScriptAsExecuteStoredProcedure() + { + string query = @"CREATE PROCEDURE testSp1 + @BusinessEntityID [int], + @JobTitle [nvarchar](50), + @HireDate [datetime], + @RateChangeDate [datetime], + @Rate [money], + @PayFrequency [tinyint] + AS + BEGIN Select * from sys.all_columns END"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Execute; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testSp1", + Schema = "dbo", + Type = "StoredProcedure" + }; + string expectedScript = "EXECUTE @RC = [dbo].[testSp1]"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + [Fact] + public async void VerifyScriptAsSelectTable() + { + string query = "CREATE TABLE testTable1 (c1 int)"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Select; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testTable1", + Schema = "dbo", + Type = "Table" + }; + string expectedScript = "SELECT TOP (1000) [c1]"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + [Fact] public async void VerifyScriptAsCreateView() { string query = "CREATE VIEW testView1 AS SELECT * from sys.all_columns"; - string scriptCreateDrop = "ScriptCreate"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Create; ScriptingObject scriptingObject = new ScriptingObject { Name = "testView1", @@ -121,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting public async void VerifyScriptAsCreateStoredProcedure() { string query = "CREATE PROCEDURE testSp1 AS BEGIN Select * from sys.all_columns END"; - string scriptCreateDrop = "ScriptCreate"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Create; ScriptingObject scriptingObject = new ScriptingObject { Name = "testSp1", @@ -137,7 +193,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting public async void VerifyScriptAsDropTable() { string query = "CREATE TABLE testTable1 (c1 int)"; - string scriptCreateDrop = "ScriptDrop"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Delete; ScriptingObject scriptingObject = new ScriptingObject { Name = "testTable1", @@ -153,7 +209,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting public async void VerifyScriptAsDropView() { string query = "CREATE VIEW testView1 AS SELECT * from sys.all_columns"; - string scriptCreateDrop = "ScriptDrop"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Delete; ScriptingObject scriptingObject = new ScriptingObject { Name = "testView1", @@ -169,7 +225,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting public async void VerifyScriptAsDropStoredProcedure() { string query = "CREATE PROCEDURE testSp1 AS BEGIN Select * from sys.all_columns END"; - string scriptCreateDrop = "ScriptDrop"; + ScriptingOperationType scriptCreateDrop = ScriptingOperationType.Delete; ScriptingObject scriptingObject = new ScriptingObject { Name = "testSp1", @@ -181,7 +237,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); } - private async Task VerifyScriptAs(string query, ScriptingObject scriptingObject, string scriptCreateDrop, string expectedScript) + private async Task VerifyScriptAs(string query, ScriptingObject scriptingObject, ScriptingOperationType operation, string expectedScript) { var testDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, query, "ScriptingTests"); try @@ -196,13 +252,23 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting var scriptingParams = new ScriptingParams { OwnerUri = queryTempFile.FilePath, - ScriptDestination = "ToEditor" + ScriptDestination = "ToEditor", + Operation = operation }; + string scriptCreateOperation = "ScriptCreate"; + if(operation == ScriptingOperationType.Delete) + { + scriptCreateOperation = "ScriptDrop"; + } + else + { + scriptCreateOperation = $"Script{operation}"; + } + scriptingParams.ScriptOptions = new ScriptOptions { - ScriptCreateDrop = scriptCreateDrop, - + ScriptCreateDrop = scriptCreateOperation, }; scriptingParams.ScriptingObjects = new List @@ -212,7 +278,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting ScriptingService service = new ScriptingService(); - await service.HandleScriptingScriptAsRequest(scriptingParams, requestContext.Object); + await service.HandleScriptExecuteRequest(scriptingParams, requestContext.Object); Thread.Sleep(2000); await service.ScriptingTask; @@ -235,7 +301,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting private static bool VerifyScriptingResult(ScriptingResult result, string expected) { - return !string.IsNullOrEmpty(result.Script) && result.Script.Contains(expected); + return string.IsNullOrEmpty(expected) ? string.IsNullOrEmpty(result.Script) : !string.IsNullOrEmpty(result.Script) && result.Script.Contains(expected); } } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.TestDriver.Tests/SqlScriptPublishModelTests.cs b/test/Microsoft.SqlTools.ServiceLayer.TestDriver.Tests/SqlScriptPublishModelTests.cs index 203ceabb..eecb2ea7 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.TestDriver.Tests/SqlScriptPublishModelTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.TestDriver.Tests/SqlScriptPublishModelTests.cs @@ -211,7 +211,7 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests ScriptingResult result = await testService.Script(requestParams); ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(15)); Assert.True(parameters.HasError); - Assert.Equal("An error occurred while scripting the objects.", parameters.ErrorMessage); + Assert.True(parameters.ErrorMessage.Contains("An error occurred while scripting the objects.")); Assert.Contains("The Table '[dbo].[TableDoesNotExist]' does not exist on the server.", parameters.ErrorDetails); } } @@ -297,7 +297,6 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests OwnerUri = tempFile.FilePath, ScriptOptions = new ScriptOptions { - ScriptCreateDrop = "ScriptSelect" }, ScriptingObjects = new List { @@ -307,7 +306,8 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests Schema = "dbo", Name = "Customers", } - } + }, + Operation = ScriptingOperationType.Select }; ScriptingResult result = await testService.Script(requestParams); Assert.True(result.Script.Contains("SELECT"));