diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptAsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptAsRequest.cs new file mode 100644 index 00000000..2f4fd891 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptAsRequest.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. +// + +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/ScriptingOptions.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs index 30984688..8e056217 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Contracts/ScriptingOptions.cs @@ -15,6 +15,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptAnsiPadding { get; set; } = false; + /// + /// Returns Generate ANSI padding statements + /// + public bool? AnsiPadding { get { return ScriptAnsiPadding; } } + /// /// Append the generated script to a file /// @@ -33,6 +38,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ConvertUDDTToBaseType { get; set; } = false; + /// + /// Returns ConvertUDDTToBaseType + /// + public bool? ConvertUserDefinedDataTypesToBaseType { get { return ConvertUDDTToBaseType; } } + /// /// Generate script for dependent objects for each object scripted. /// @@ -49,6 +59,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? IncludeDescriptiveHeaders { get; set; } = true; + /// + /// Returns IncludeDescriptiveHeaders + /// + public bool? IncludeHeaders { get { return IncludeDescriptiveHeaders; } } + /// /// Check that an object with the given name exists before dropping or altering or that an object with the given name does not exist before creating. /// @@ -64,6 +79,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptDriIncludeSystemNames { get; set; } = false; + /// + /// Returns ScriptDriIncludeSystemNames + /// + public bool? DriIncludeSystemNames { get { return ScriptDriIncludeSystemNames; } } + /// /// Include statements in the script that are not supported on the specified SQL Server database engine type. /// @@ -77,6 +97,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? SchemaQualify { get; set; } = true; + /// + /// Returns SchemaQualify + /// + public bool? SchemaQualifyForeignKeysReferences { get { return SchemaQualify; } } + /// /// Script options to set bindings option. /// @@ -87,6 +112,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? Collation { get; set; } = false; + /// + /// Returns false if Collation is true + /// + public bool? NoCollation { get { return !Collation; } } + /// /// Script the default values. /// @@ -95,6 +125,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? Default { get; set; } = true; + /// + /// Returns the value of Default Property + /// + public bool? DriDefaults { get { return Default; } } + + /// /// Script Object CREATE/DROP statements. /// Possible values: @@ -116,6 +152,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptExtendedProperties { get; set; } = true; + /// + /// Returns the value of ScriptExtendedProperties Property + /// + public bool? ExtendedProperties { get { return ScriptExtendedProperties; } } + + /// /// Script only features compatible with the specified version of SQL Server. Possible values: /// Script90Compat @@ -162,6 +204,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptObjectLevelPermissions { get; set; } = false; + /// + /// Returns the value of ScriptObjectLevelPermissions Property + /// + public bool? Permissions { get { return ScriptObjectLevelPermissions; } } + /// /// Script owner for the objects. /// @@ -179,6 +226,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public string ScriptStatistics { get; set; } = "ScriptStatsNone"; + /// + /// Returns the value of ScriptStatistics Property + /// + public string Statistics { get { return ScriptStatistics; } } + + /// /// Generate USE DATABASE statement. /// @@ -201,6 +254,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptChangeTracking { get; set; } = false; + /// + /// Returns the value of ScriptChangeTracking Property + /// + public bool? ChangeTracking { get { return ScriptChangeTracking; } } + + /// /// Script the check constraints for each table or view scripted. /// @@ -209,11 +268,22 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptCheckConstraints { get; set; } = true; + /// + /// Returns the value of ScriptCheckConstraints Property + /// + public bool? DriChecks { get { return ScriptCheckConstraints; } } + /// /// Scripts the data compression information. /// public bool? ScriptDataCompressionOptions { get; set; } = false; + /// + /// Returns the value of ScriptDataCompressionOptions Property + /// + public bool? ScriptDataCompression { get { return ScriptDataCompressionOptions; } } + + /// /// Script the foreign keys for each table scripted. /// @@ -222,11 +292,23 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptForeignKeys { get; set; } = true; + /// + /// Returns the value of ScriptForeignKeys Property + /// + public bool? DriForeignKeys { get { return ScriptForeignKeys; } } + + /// /// Script the full-text indexes for each table or indexed view scripted. /// public bool? ScriptFullTextIndexes { get; set; } = true; + /// + /// Returns the value of ScriptFullTextIndexes Property + /// + public bool? FullTextIndexes { get { return ScriptFullTextIndexes; } } + + /// /// Script the indexes (including XML and clustered indexes) for each table or indexed view scripted. /// @@ -235,6 +317,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptIndexes { get; set; } = true; + /// + /// Returns the value of ScriptIndexes Property + /// + public bool? DriIndexes { get { return ScriptIndexes; } } + + /// /// Script the primary keys for each table or view scripted /// @@ -243,11 +331,23 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// public bool? ScriptPrimaryKeys { get; set; } = true; + /// + /// Returns the value of ScriptPrimaryKeys Property + /// + public bool? DriPrimaryKey { get { return ScriptPrimaryKeys; } } + + /// /// Script the triggers for each table or view scripted /// public bool? ScriptTriggers { get; set; } = true; + /// + /// Returns the value of ScriptTriggers Property + /// + public bool? Triggers { get { return ScriptTriggers; } } + + /// /// Script the unique keys for each table or view scripted. /// @@ -255,5 +355,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts /// The default value is true. /// public bool? UniqueKeys { get; set; } = true; + + /// + /// Returns the value of UniqueKeys Property + /// + public bool? DriUniqueKeys { get { return UniqueKeys; } } + } } \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs new file mode 100644 index 00000000..84aaeda2 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptAsScriptingOperation.cs @@ -0,0 +1,318 @@ +// +// 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.Data.SqlClient; +using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts; +using Microsoft.SqlTools.Utility; +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlServer.Management.Smo; +using System.Collections.Specialized; +using System.Text; +using System.Globalization; +using Microsoft.SqlServer.Management.SqlScriptPublish; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Scripting +{ + /// + /// Class to generate script as for one smo object + /// + public class ScriptAsScriptingOperation : SmoScriptingOperation + { + private static Dictionary scriptCompatabilityMap = LoadScriptCompatabilityMap(); + + public ScriptAsScriptingOperation(ScriptingParams parameters): base(parameters) + { + } + + public override void Execute() + { + SqlServer.Management.Smo.Scripter scripter = null; + try + { + this.CancellationToken.ThrowIfCancellationRequested(); + + this.ValidateScriptDatabaseParams(); + + this.CancellationToken.ThrowIfCancellationRequested(); + string resultScript = string.Empty; + // TODO: try to use one of the existing connections + using (SqlConnection sqlConnection = new SqlConnection(this.Parameters.ConnectionString)) + { + sqlConnection.Open(); + ServerConnection serverConnection = new ServerConnection(sqlConnection); + Server server = new Server(serverConnection); + scripter = new SqlServer.Management.Smo.Scripter(server); + ScriptingOptions options = new ScriptingOptions(); + SetScriptBehavior(options); + PopulateAdvancedScriptOptions(this.Parameters.ScriptOptions, options); + options.WithDependencies = false; + 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); + } + + this.CancellationToken.ThrowIfCancellationRequested(); + + Logger.Write( + LogLevel.Verbose, + string.Format( + "Sending script complete notification event for operation {0}", + this.OperationId + )); + + ScriptText = resultScript; + + this.SendCompletionNotificationEvent(new ScriptingCompleteParams + { + Success = true, + }); + } + catch (Exception e) + { + if (e.IsOperationCanceledException()) + { + Logger.Write(LogLevel.Normal, string.Format("Scripting operation {0} was canceled", this.OperationId)); + this.SendCompletionNotificationEvent(new ScriptingCompleteParams + { + Canceled = true, + }); + } + else + { + Logger.Write(LogLevel.Error, string.Format("Scripting operation {0} failed with exception {1}", this.OperationId, e)); + this.SendCompletionNotificationEvent(new ScriptingCompleteParams + { + OperationId = OperationId, + HasError = true, + ErrorMessage = e.Message, + ErrorDetails = e.ToString(), + }); + } + } + finally + { + if (scripter != null) + { + scripter.ScriptingError -= this.ScripterScriptingError; + } + } + } + + private string GetScript(ScriptingOptions options, StringCollection stringCollection) + { + StringBuilder sb = new StringBuilder(); + + foreach (var item in stringCollection) + { + sb.Append(item); + if (options != null && !options.NoCommandTerminator) + { + //Ensure the batch separator is always on a new line (to avoid syntax errors) + //but don't write an extra if we already have one as this can affect definitions + //of objects such as Stored Procedures (see TFS#9125366) + sb.AppendFormat(CultureInfo.InvariantCulture, "{0}{1}{2}", + item.EndsWith(Environment.NewLine) ? string.Empty : Environment.NewLine, + CommonConstants.DefaultBatchSeperator, + Environment.NewLine); + } + else + { + sb.AppendFormat(CultureInfo.InvariantCulture, Environment.NewLine); + } + } + + return sb.ToString(); + } + + private UrnCollection CreateUrns(ServerConnection serverConnection) + { + IEnumerable selectedObjects = new List(this.Parameters.ScriptingObjects); + + string server = serverConnection.TrueName; + string database = new SqlConnectionStringBuilder(this.Parameters.ConnectionString).InitialCatalog; + UrnCollection urnCollection = new UrnCollection(); + foreach (var scriptingObject in selectedObjects) + { + if(string.IsNullOrEmpty(scriptingObject.Schema)) + { + // TODO: get the default schema + scriptingObject.Schema = "dbo"; + } + urnCollection.Add(scriptingObject.ToUrn(server, database)); + } + return urnCollection; + } + + private void SetScriptBehavior(ScriptingOptions options) + { + // TODO: have to add Scripting behavior to Smo ScriptingOptions class + // so it would support ScriptDropAndScreate + switch (this.Parameters.ScriptOptions.ScriptCreateDrop) + { + case "ScriptCreate": + options.ScriptDrops = false; + break; + case "ScriptDrop": + options.ScriptDrops = true; + break; + default: + options.ScriptDrops = false; + break; + + } + } + + private static Dictionary LoadScriptCompatabilityMap() + { + Dictionary map = new Dictionary(); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script140Compat.ToString(), SqlServerVersion.Version140); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script130Compat.ToString(), SqlServerVersion.Version130); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script120Compat.ToString(), SqlServerVersion.Version120); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script110Compat.ToString(), SqlServerVersion.Version110); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script105Compat.ToString(), SqlServerVersion.Version105); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script100Compat.ToString(), SqlServerVersion.Version100); + map.Add(SqlScriptOptions.ScriptCompatabilityOptions.Script90Compat.ToString(), SqlServerVersion.Version90); + + return map; + } + + private void SetScriptingOptions(ScriptingOptions scriptingOptions) + { + scriptingOptions.AllowSystemObjects = true; + + // setting this forces SMO to correctly script objects that have been renamed + scriptingOptions.EnforceScriptingOptions = true; + + //We always want role memberships for users and database roles to be scripted + scriptingOptions.IncludeDatabaseRoleMemberships = true; + SqlServerVersion targetServerVersion; + if(scriptCompatabilityMap.TryGetValue(this.Parameters.ScriptOptions.ScriptCompatibilityOption, out targetServerVersion)) + { + scriptingOptions.TargetServerVersion = targetServerVersion; + } + else + { + //If you are getting this assertion fail it means you are working for higher + //version of SQL Server. You need to update this part of code. + Logger.Write(LogLevel.Warning, "This part of the code is not updated corresponding to latest version change"); + } + + // for cloud scripting to work we also have to have Script Compat set to 105. + // the defaults from scripting options should take care of it + object targetDatabaseEngineType; + if (Enum.TryParse(typeof(SqlScriptOptions.ScriptDatabaseEngineType), this.Parameters.ScriptOptions.TargetDatabaseEngineType, out targetDatabaseEngineType)) + { + switch ((SqlScriptOptions.ScriptDatabaseEngineType)targetDatabaseEngineType) + { + case SqlScriptOptions.ScriptDatabaseEngineType.SingleInstance: + scriptingOptions.TargetDatabaseEngineType = DatabaseEngineType.Standalone; + break; + case SqlScriptOptions.ScriptDatabaseEngineType.SqlAzure: + scriptingOptions.TargetDatabaseEngineType = DatabaseEngineType.SqlAzureDatabase; + break; + } + } + + object targetDatabaseEngineEdition; + if (Enum.TryParse(typeof(SqlScriptOptions.ScriptDatabaseEngineEdition), this.Parameters.ScriptOptions.TargetDatabaseEngineEdition, out targetDatabaseEngineEdition)) + { + switch ((SqlScriptOptions.ScriptDatabaseEngineEdition)targetDatabaseEngineEdition) + { + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlServerPersonalEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.Personal; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlServerStandardEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.Standard; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlServerEnterpriseEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.Enterprise; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlServerExpressEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.Express; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlAzureDatabaseEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.SqlDatabase; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlDatawarehouseEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.SqlDataWarehouse; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlServerStretchEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.SqlStretchDatabase; + break; + case SqlScriptOptions.ScriptDatabaseEngineEdition.SqlServerManagedInstanceEdition: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.SqlManagedInstance; + break; + default: + scriptingOptions.TargetDatabaseEngineEdition = DatabaseEngineEdition.Standard; + break; + } + } + + scriptingOptions.NoVardecimal = false; //making IncludeVarDecimal true for DPW + + // scripting of stats is a combination of the Statistics + // and the OptimizerData flag + object scriptStatistics; + if (Enum.TryParse(typeof(SqlScriptOptions.ScriptStatisticsOptions), this.Parameters.ScriptOptions.ScriptStatistics, out scriptStatistics)) + { + switch ((SqlScriptOptions.ScriptStatisticsOptions)scriptStatistics) + { + case SqlScriptOptions.ScriptStatisticsOptions.ScriptStatsAll: + scriptingOptions.Statistics = true; + scriptingOptions.OptimizerData = true; + break; + case SqlScriptOptions.ScriptStatisticsOptions.ScriptStatsDDL: + scriptingOptions.Statistics = true; + scriptingOptions.OptimizerData = false; + break; + case SqlScriptOptions.ScriptStatisticsOptions.ScriptStatsNone: + scriptingOptions.Statistics = false; + scriptingOptions.OptimizerData = false; + break; + } + } + + // If Histogram and Update Statics are True then include DriIncludeSystemNames and AnsiPadding by default + if (scriptingOptions.Statistics == true && scriptingOptions.OptimizerData == true) + { + scriptingOptions.DriIncludeSystemNames = true; + scriptingOptions.AnsiPadding = true; + } + } + + private void ScripterScriptingError(object sender, ScriptingErrorEventArgs e) + { + this.CancellationToken.ThrowIfCancellationRequested(); + + Logger.Write( + LogLevel.Verbose, + string.Format( + "Sending scripting error progress event, Urn={0}, OperationId={1}, Completed={2}, Error={3}", + e.Current, + this.OperationId, + false, + e?.InnerException?.ToString() ?? "null")); + + this.SendProgressNotificationEvent(new ScriptingProgressNotificationParams + { + ScriptingObject = e.Current?.ToScriptingObject(), + Status = "Failed", + ErrorMessage = e?.InnerException?.Message, + ErrorDetails = e?.InnerException?.ToString(), + }); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs index 388463a1..abf54cff 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/Scripter.cs @@ -23,7 +23,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting // Mapping for supported type AddSupportedType(DeclarationType.Table, "Table", "table", typeof(Table)); AddSupportedType(DeclarationType.View, "View", "view", typeof(View)); - AddSupportedType(DeclarationType.StoredProcedure, "Procedure", "stored procedure", typeof(StoredProcedure)); + AddSupportedType(DeclarationType.StoredProcedure, "StoredProcedure", "stored procedure", typeof(StoredProcedure)); AddSupportedType(DeclarationType.Schema, "Schema", "schema", typeof(Schema)); AddSupportedType(DeclarationType.UserDefinedDataType, "UserDefinedDataType", "user-defined data type", typeof(UserDefinedDataType)); AddSupportedType(DeclarationType.UserDefinedTableType, "UserDefinedTableType", "user-defined table type", typeof(UserDefinedTableType)); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScripterCore.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScripterCore.cs index fa6903d3..d212a927 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScripterCore.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScripterCore.cs @@ -273,7 +273,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting string tempFileName = (schemaName != null) ? Path.Combine(this.tempPath, string.Format("{0}.{1}.sql", schemaName, objectName)) : Path.Combine(this.tempPath, string.Format("{0}.sql", objectName)); - ScriptingScriptOperation operation = InitScriptOperation(objectName, schemaName, objectType); + SmoScriptingOperation operation = InitScriptOperation(objectName, schemaName, objectType); operation.Execute(); string script = operation.ScriptText; @@ -286,7 +286,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting string createSyntax = null; if (objectScriptMap.ContainsKey(objectType.ToLower())) { - createSyntax = string.Format("CREATE {0}", objectScriptMap[objectType.ToLower()]); + createSyntax = string.Format("CREATE"); foreach (string line in lines) { if (LineContainsObject(line, objectName, createSyntax)) @@ -458,7 +458,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting /// /// /// - internal ScriptingScriptOperation InitScriptOperation(string objectName, string schemaName, string objectType) + internal SmoScriptingOperation InitScriptOperation(string objectName, string schemaName, string objectType) { // object that has to be scripted ScriptingObject scriptingObject = new ScriptingObject @@ -507,7 +507,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting ScriptDestination = "ToEditor" }; - return new ScriptingScriptOperation(parameters); + return new ScriptAsScriptingOperation(parameters); } internal string GetTargetDatabaseEngineEdition() diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs index 466f1d67..a9d1bf65 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingScriptOperation.cs @@ -6,23 +6,18 @@ using System; using System.Collections.Generic; using System.Data.SqlClient; -using System.IO; using System.Linq; -using System.Reflection; using Microsoft.SqlServer.Management.SqlScriptPublish; using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts; using Microsoft.SqlTools.Utility; -using static Microsoft.SqlServer.Management.SqlScriptPublish.SqlScriptOptions; -using Microsoft.SqlServer.Management.Common; namespace Microsoft.SqlTools.ServiceLayer.Scripting { /// /// Class to represent an in-progress script operation. /// - public sealed class ScriptingScriptOperation : ScriptingOperation + public sealed class ScriptingScriptOperation : SmoScriptingOperation { - private bool disposed = false; private int scriptedObjectCount = 0; @@ -30,35 +25,15 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting private int eventSequenceNumber = 1; - public ScriptingScriptOperation(ScriptingParams parameters) + public ScriptingScriptOperation(ScriptingParams parameters): base(parameters) { - Validate.IsNotNull("parameters", parameters); - - this.Parameters = parameters; } - private ScriptingParams Parameters { get; set; } - - public string ScriptText { get; private set; } - /// /// Event raised when a scripting operation has resolved which database objects will be scripted. /// public event EventHandler PlanNotification; - /// - /// Event raised when a scripting operation has made forward progress. - /// - public event EventHandler ProgressNotification; - - /// - /// Event raised when a scripting operation is complete. - /// - /// - /// An event can be completed by the following conditions: success, cancel, error. - /// - public event EventHandler CompleteNotification; - public override void Execute() { SqlScriptPublishModel publishModel = null; @@ -141,10 +116,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } } - private void SendCompletionNotificationEvent(ScriptingCompleteParams parameters) + protected override void SendCompletionNotificationEvent(ScriptingCompleteParams parameters) { this.SetCommonEventProperties(parameters); - this.CompleteNotification?.Invoke(this, parameters); + base.SendCompletionNotificationEvent(parameters); } private void SendPlanNotificationEvent(ScriptingPlanNotificationParams parameters) @@ -153,10 +128,10 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting this.PlanNotification?.Invoke(this, parameters); } - private void SendProgressNotificationEvent(ScriptingProgressNotificationParams parameters) + protected override void SendProgressNotificationEvent(ScriptingProgressNotificationParams parameters) { this.SetCommonEventProperties(parameters); - this.ProgressNotification?.Invoke(this, parameters); + base.SendProgressNotificationEvent(parameters); } private void SetCommonEventProperties(ScriptingEventParams parameters) @@ -242,108 +217,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting return publishModel; } - private string GetServerNameFromLiveInstance(string connectionString) - { - string serverName = null; - using(SqlConnection connection = new SqlConnection(connectionString)) - { - connection.Open(); - - try - { - - ServerConnection serverConnection = new ServerConnection(connection); - serverName = serverConnection.TrueName; - } - catch (SqlException e) - { - Logger.Write( - LogLevel.Verbose, - string.Format("Exception getting server name", e)); - } - } - - Logger.Write(LogLevel.Verbose, string.Format("Resolved server name '{0}'", serverName)); - return serverName; - } - - private static void PopulateAdvancedScriptOptions(ScriptOptions scriptOptionsParameters, SqlScriptOptions advancedOptions) - { - if (scriptOptionsParameters == null) - { - Logger.Write(LogLevel.Verbose, "No advanced options set, the ScriptOptions object is null."); - return; - } - - foreach (PropertyInfo optionPropInfo in scriptOptionsParameters.GetType().GetProperties()) - { - PropertyInfo advancedOptionPropInfo = advancedOptions.GetType().GetProperty(optionPropInfo.Name); - if (advancedOptionPropInfo == null) - { - Logger.Write(LogLevel.Warning, string.Format("Invalid property info name {0} could not be mapped to a property on SqlScriptOptions.", optionPropInfo.Name)); - continue; - } - - object optionValue = optionPropInfo.GetValue(scriptOptionsParameters, index: null); - if (optionValue == null) - { - Logger.Write(LogLevel.Verbose, string.Format("Skipping ScriptOptions.{0} since value is null", optionPropInfo.Name)); - continue; - } - - // - // The ScriptOptions property types from the request will be either a string or a bool?. - // The SqlScriptOptions property types from SMO will all be an Enum. Using reflection, we - // map the request ScriptOptions values to the SMO SqlScriptOptions values. - // - - try - { - object smoValue = null; - if (optionPropInfo.PropertyType == typeof(bool?)) - { - smoValue = (bool)optionValue ? BooleanTypeOptions.True : BooleanTypeOptions.False; - } - else - { - smoValue = Enum.Parse(advancedOptionPropInfo.PropertyType, (string)optionValue, ignoreCase: true); - } - - Logger.Write(LogLevel.Verbose, string.Format("Setting ScriptOptions.{0} to value {1}", optionPropInfo.Name, smoValue)); - advancedOptionPropInfo.SetValue(advancedOptions, smoValue); - } - catch (Exception e) - { - Logger.Write( - LogLevel.Warning, - string.Format("An exception occurred setting option {0} to value {1}: {2}", optionPropInfo.Name, optionValue, e)); - } - } - } - - private void ValidateScriptDatabaseParams() - { - try - { - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(this.Parameters.ConnectionString); - } - catch (Exception e) - { - throw new ArgumentException(SR.ScriptingParams_ConnectionString_Property_Invalid, e); - } - if (this.Parameters.FilePath == null && this.Parameters.ScriptDestination != "ToEditor") - { - throw new ArgumentException(SR.ScriptingParams_FilePath_Property_Invalid); - } - else if (this.Parameters.FilePath != null && this.Parameters.ScriptDestination != "ToEditor") - { - if (!Directory.Exists(Path.GetDirectoryName(this.Parameters.FilePath))) - { - throw new ArgumentException(SR.ScriptingParams_FilePath_Property_Invalid); - } - } - } - private void OnPublishModelScriptError(object sender, ScriptEventArgs e) { this.CancellationToken.ThrowIfCancellationRequested(); @@ -425,16 +298,5 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting }); } - /// - /// Disposes the scripting operation. - /// - public override void Dispose() - { - if (!disposed) - { - this.Cancel(); - disposed = true; - } - } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs index ab9745b2..75253042 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs @@ -23,6 +23,7 @@ using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlTools.ServiceLayer.Utility; +using Microsoft.SqlTools.ServiceLayer.LanguageServices; namespace Microsoft.SqlTools.ServiceLayer.Scripting { @@ -37,7 +38,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting public static ScriptingService Instance => LazyInstance.Value; - private static ConnectionService connectionService = null; + private static ConnectionService connectionService = null; private readonly Lazy> operations = new Lazy>(() => new ConcurrentDictionary()); @@ -61,8 +62,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { connectionService = value; } - } - + } /// /// The collection of active operations @@ -76,6 +76,7 @@ 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); @@ -108,6 +109,52 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } } + /// + /// Handles request to start the scripting operation + /// + public async Task HandleScriptingScriptAsRequest(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") + { + 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 + { + ScriptAsScriptingOperation operation = new ScriptAsScriptingOperation(parameters); + 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); + } + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + /// /// Handles request to start the scripting operation /// @@ -180,7 +227,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } private async void SendScriptingCompleteEvent(RequestContext requestContext, EventType eventType, TParams parameters, - ScriptingScriptOperation operation, string scriptDestination) + SmoScriptingOperation operation, string scriptDestination) { await requestContext.SendEvent(eventType, parameters); switch (scriptDestination) @@ -283,7 +330,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting /// private void RunTask(RequestContext context, ScriptingOperation operation) { - Task.Run(async () => + ScriptingTask = Task.Run(async () => { try { @@ -302,6 +349,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting }).ContinueWithOnFaulted(async t => await context.SendError(t.Exception)); } + internal Task ScriptingTask { get; set; } + /// /// Disposes the scripting service and all active scripting operations. /// @@ -318,4 +367,4 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs new file mode 100644 index 00000000..5dd9afb8 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/SmoScriptingOperation.cs @@ -0,0 +1,179 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlServer.Management.Common; +using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts; +using Microsoft.SqlTools.Utility; +using System; +using System.Data.SqlClient; +using System.IO; +using System.Reflection; +using static Microsoft.SqlServer.Management.SqlScriptPublish.SqlScriptOptions; + +namespace Microsoft.SqlTools.ServiceLayer.Scripting +{ + /// + /// Base class for all SMO scripting operations + /// + public abstract class SmoScriptingOperation : ScriptingOperation + { + private bool disposed = false; + + public SmoScriptingOperation(ScriptingParams parameters) + { + Validate.IsNotNull("parameters", parameters); + + this.Parameters = parameters; + } + + protected ScriptingParams Parameters { get; set; } + + public string ScriptText { get; protected set; } + + /// + /// An event can be completed by the following conditions: success, cancel, error. + /// + public event EventHandler CompleteNotification; + + /// + /// Event raised when a scripting operation has made forward progress. + /// + public event EventHandler ProgressNotification; + + protected virtual void SendCompletionNotificationEvent(ScriptingCompleteParams parameters) + { + this.CompleteNotification?.Invoke(this, parameters); + } + + protected virtual void SendProgressNotificationEvent(ScriptingProgressNotificationParams parameters) + { + this.ProgressNotification?.Invoke(this, parameters); + } + + protected string GetServerNameFromLiveInstance(string connectionString) + { + string serverName = null; + using (SqlConnection connection = new SqlConnection(connectionString)) + { + connection.Open(); + + try + { + + ServerConnection serverConnection = new ServerConnection(connection); + serverName = serverConnection.TrueName; + } + catch (SqlException e) + { + Logger.Write( + LogLevel.Verbose, + string.Format("Exception getting server name", e)); + } + } + + Logger.Write(LogLevel.Verbose, string.Format("Resolved server name '{0}'", serverName)); + return serverName; + } + + protected void ValidateScriptDatabaseParams() + { + try + { + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(this.Parameters.ConnectionString); + } + catch (Exception e) + { + throw new ArgumentException(SR.ScriptingParams_ConnectionString_Property_Invalid, e); + } + if (this.Parameters.FilePath == null && this.Parameters.ScriptDestination != "ToEditor") + { + throw new ArgumentException(SR.ScriptingParams_FilePath_Property_Invalid); + } + else if (this.Parameters.FilePath != null && this.Parameters.ScriptDestination != "ToEditor") + { + if (!Directory.Exists(Path.GetDirectoryName(this.Parameters.FilePath))) + { + throw new ArgumentException(SR.ScriptingParams_FilePath_Property_Invalid); + } + } + } + + protected static void PopulateAdvancedScriptOptions(ScriptOptions scriptOptionsParameters, object advancedOptions) + { + if (scriptOptionsParameters == null) + { + Logger.Write(LogLevel.Verbose, "No advanced options set, the ScriptOptions object is null."); + return; + } + + foreach (PropertyInfo optionPropInfo in scriptOptionsParameters.GetType().GetProperties()) + { + PropertyInfo advancedOptionPropInfo = advancedOptions.GetType().GetProperty(optionPropInfo.Name); + if (advancedOptionPropInfo == null) + { + Logger.Write(LogLevel.Warning, string.Format("Invalid property info name {0} could not be mapped to a property on SqlScriptOptions.", optionPropInfo.Name)); + continue; + } + + object optionValue = optionPropInfo.GetValue(scriptOptionsParameters, index: null); + if (optionValue == null) + { + Logger.Write(LogLevel.Verbose, string.Format("Skipping ScriptOptions.{0} since value is null", optionPropInfo.Name)); + continue; + } + + // + // The ScriptOptions property types from the request will be either a string or a bool?. + // The SqlScriptOptions property types from SMO will all be an Enum. Using reflection, we + // map the request ScriptOptions values to the SMO SqlScriptOptions values. + // + + try + { + object smoValue = null; + if (optionPropInfo.PropertyType == typeof(bool?)) + { + if (advancedOptionPropInfo.PropertyType == typeof(bool)) + { + + smoValue = (bool)optionValue; + } + else + { + smoValue = (bool)optionValue ? BooleanTypeOptions.True : BooleanTypeOptions.False; + } + } + else + { + smoValue = Enum.Parse(advancedOptionPropInfo.PropertyType, (string)optionValue, ignoreCase: true); + } + + Logger.Write(LogLevel.Verbose, string.Format("Setting ScriptOptions.{0} to value {1}", optionPropInfo.Name, smoValue)); + advancedOptionPropInfo.SetValue(advancedOptions, smoValue); + } + catch (Exception e) + { + Logger.Write( + LogLevel.Warning, + string.Format("An exception occurred setting option {0} to value {1}: {2}", optionPropInfo.Name, optionValue, e)); + } + } + + } + + /// + /// Disposes the scripting operation. + /// + public override void Dispose() + { + if (!disposed) + { + this.Cancel(); + disposed = true; + } + } + + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/CommonConstants.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/CommonConstants.cs index d98f75ee..05908619 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/CommonConstants.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/CommonConstants.cs @@ -15,5 +15,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility public const string MsdbDatabaseName = "msdb"; public const string ModelDatabaseName = "model"; public const string TempDbDatabaseName = "tempdb"; + + public const string DefaultBatchSeperator = "GO"; } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs index fe7a53b4..364b2d63 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Scripting/ScriptingServiceTests.cs @@ -3,17 +3,19 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; -using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.ServiceLayer.Scripting; using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Moq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Xunit; - +using static Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility.LiveConnectionHelper; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting { @@ -27,7 +29,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting private const string ViewName = "test"; private const string DatabaseName = "test-db"; private const string StoredProcName = "test-sp"; - private string[] objects = new string[5] {"Table", "View", "Schema", "Database", "SProc"}; + private string[] objects = new string[5] { "Table", "View", "Schema", "Database", "SProc" }; private string[] selectObjects = new string[2] { "Table", "View" }; private LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects() @@ -82,5 +84,158 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting Assert.NotNull(await SendAndValidateScriptRequest(true)); } } + + [Fact] + public async void VerifyScriptAsCreateTable() + { + string query = "CREATE TABLE testTable1 (c1 int)"; + string scriptCreateDrop = "ScriptCreate"; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testTable1", + Schema = "dbo", + Type = "Table" + }; + string expectedScript = "CREATE TABLE [dbo].[testTable1]"; + + 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"; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testView1", + Schema = "dbo", + Type = "View" + }; + string expectedScript = "CREATE VIEW [dbo].[testView1] AS"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + [Fact] + public async void VerifyScriptAsCreateStoredProcedure() + { + string query = "CREATE PROCEDURE testSp1 AS BEGIN Select * from sys.all_columns END"; + string scriptCreateDrop = "ScriptCreate"; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testSp1", + Schema = "dbo", + Type = "StoredProcedure" + }; + string expectedScript = "CREATE PROCEDURE [dbo].[testSp1] AS"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + [Fact] + public async void VerifyScriptAsDropTable() + { + string query = "CREATE TABLE testTable1 (c1 int)"; + string scriptCreateDrop = "ScriptDrop"; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testTable1", + Schema = "dbo", + Type = "Table" + }; + string expectedScript = "DROP TABLE [dbo].[testTable1]"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + [Fact] + public async void VerifyScriptAsDropView() + { + string query = "CREATE VIEW testView1 AS SELECT * from sys.all_columns"; + string scriptCreateDrop = "ScriptDrop"; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testView1", + Schema = "dbo", + Type = "View" + }; + string expectedScript = "DROP VIEW [dbo].[testView1]"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + [Fact] + public async void VerifyScriptAsDropStoredProcedure() + { + string query = "CREATE PROCEDURE testSp1 AS BEGIN Select * from sys.all_columns END"; + string scriptCreateDrop = "ScriptDrop"; + ScriptingObject scriptingObject = new ScriptingObject + { + Name = "testSp1", + Schema = "dbo", + Type = "StoredProcedure" + }; + string expectedScript = "DROP PROCEDURE [dbo].[testSp1]"; + + await VerifyScriptAs(query, scriptingObject, scriptCreateDrop, expectedScript); + } + + private async Task VerifyScriptAs(string query, ScriptingObject scriptingObject, string scriptCreateDrop, string expectedScript) + { + var testDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, query, "ScriptingTests"); + try + { + var requestContext = new Mock>(); + requestContext.Setup(x => x.SendResult(It.IsAny())).Returns(Task.FromResult(new object())); + ConnectionService connectionService = LiveConnectionHelper.GetLiveTestConnectionService(); + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + //Opening a connection to db to lock the db + TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync(testDb.DatabaseName, queryTempFile.FilePath, ConnectionType.Default); + var scriptingParams = new ScriptingParams + { + OwnerUri = queryTempFile.FilePath, + ScriptDestination = "ToEditor" + }; + + scriptingParams.ScriptOptions = new ScriptOptions + { + ScriptCreateDrop = scriptCreateDrop, + + }; + + scriptingParams.ScriptingObjects = new List + { + scriptingObject + }; + + + ScriptingService service = new ScriptingService(); + await service.HandleScriptingScriptAsRequest(scriptingParams, requestContext.Object); + Thread.Sleep(2000); + await service.ScriptingTask; + + requestContext.Verify(x => x.SendResult(It.Is(r => VerifyScriptingResult(r, expectedScript)))); + connectionService.Disconnect(new ServiceLayer.Connection.Contracts.DisconnectParams + { + OwnerUri = queryTempFile.FilePath + }); + } + } + catch + { + throw; + } + finally + { + await testDb.CleanupAsync(); + } + } + + private static bool VerifyScriptingResult(ScriptingResult result, string expected) + { + return !string.IsNullOrEmpty(result.Script) && result.Script.Contains(expected); + } } }