Add scripting API implemented by the SqlScriptPublishModel (#316)

Update the ScriptingService to expose new scripting JSON-RPC APIs that use the SqlScriptPublishModel for script generation.

The SqlScriptPublishModel is the model behind the SSMS scripting wizard. To enable scripting for CLI tools, we've ported SqlScriptPublishModel to .NET Core. The SqlScriptPublishModel wraps the SMO scripting APIs for .sql script generation.

1) Added three new requests to the ScriptingService: ScriptingRequest, ScriptingListObjectsRequest, ScriptingCancelRequest.
2) Generating scripts are long running operations, so the ScriptingRequest and ScriptingListObjectsRequest kick off a long running scripting task and return immediately.
3) Long running scripting task reports progress and completion, and can be cancelled by a ScriptingCancelRequest request.
4) Bumped the SMO nuget package to 140.17049.0. This new version contains a signed SSMS_Rel build of SMO with the SqlScriptPublishModel.
5) For testing, adding the Northwind database schema

TODO (in later pull requests)
1) Integrate the new ScriptingService APIs with the ConnectionService
2) Integrate with the metadata support recently added
This commit is contained in:
Brian O'Neill
2017-04-24 16:10:20 -07:00
committed by GitHub
parent e65699ef75
commit 4aac4a4047
42 changed files with 7124 additions and 30 deletions

Binary file not shown.

View File

@@ -12,7 +12,7 @@
"Newtonsoft.Json": "9.0.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.4.0-sqltools-24613-04",
"Microsoft.SqlServer.Smo": "14.0.17028",
"Microsoft.SqlServer.Smo": "140.17049.0",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",

View File

@@ -5,7 +5,7 @@
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.SqlServer.Smo": "14.0.17028"
"Microsoft.SqlServer.Smo": "140.17049.0"
},
"frameworks": {
"netcoreapp1.0": {

View File

@@ -56,4 +56,26 @@ namespace Microsoft.SqlTools.Utility
return obj.HasValue && obj.Value;
}
}
public static class ExceptionExtensions
{
/// <summary>
/// Returns true if the passed exception or any inner exception is an OperationCanceledException instance.
/// </summary>
public static bool IsOperationCanceledException(this Exception e)
{
Exception current = e;
while (current != null)
{
if (current is OperationCanceledException)
{
return true;
}
current = current.InnerException;
}
return false;
}
}
}

View File

@@ -2157,6 +2157,30 @@ namespace Microsoft.SqlTools.ServiceLayer
}
}
public static string ScriptingParams_ConnectionString_Property_Invalid
{
get
{
return Keys.GetString(Keys.ScriptingParams_ConnectionString_Property_Invalid);
}
}
public static string ScriptingParams_FilePath_Property_Invalid
{
get
{
return Keys.GetString(Keys.ScriptingParams_FilePath_Property_Invalid);
}
}
public static string ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid
{
get
{
return Keys.GetString(Keys.ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid);
}
}
public static string ConnectionServiceListDbErrorNotConnected(string uri)
{
return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri);
@@ -3075,6 +3099,15 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string SchemaHierarchy_ColumnEncryptionKeys = "SchemaHierarchy_ColumnEncryptionKeys";
public const string ScriptingParams_ConnectionString_Property_Invalid = "ScriptingParams_ConnectionString_Property_Invalid";
public const string ScriptingParams_FilePath_Property_Invalid = "ScriptingParams_FilePath_Property_Invalid";
public const string ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid = "ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid";
private Keys()
{ }

View File

@@ -1251,4 +1251,16 @@
<value>Column Encryption Keys</value>
<comment></comment>
</data>
<data name="ScriptingParams_ConnectionString_Property_Invalid" xml:space="preserve">
<value>Error parsing ScriptingParams.ConnectionString property.</value>
<comment></comment>
</data>
<data name="ScriptingParams_FilePath_Property_Invalid" xml:space="preserve">
<value>Invalid directory specified by the ScriptingParams.FilePath property.</value>
<comment></comment>
</data>
<data name="ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid" xml:space="preserve">
<value>Error parsing ScriptingListObjectsCompleteParams.ConnectionString property.</value>
<comment></comment>
</data>
</root>

View File

@@ -623,3 +623,11 @@ SchemaHierarchy_ColumnMasterKeys = Column Master Keys
SchemaHierarchy_ColumnEncryptionKeys = Column Encryption Keys
############################################################################
# Scripting Service
ScriptingParams_ConnectionString_Property_Invalid = Error parsing ScriptingParams.ConnectionString property.
ScriptingParams_FilePath_Property_Invalid = Invalid directory specified by the ScriptingParams.FilePath property.
ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid = Error parsing ScriptingListObjectsCompleteParams.ConnectionString property.

View File

@@ -1416,6 +1416,21 @@
<target state="new">Server</target>
<note></note>
</trans-unit>
<trans-unit id="ScriptingParams_ConnectionString_Property_Invalid">
<source>Error parsing ScriptingParams.ConnectionString property.</source>
<target state="new">Error parsing ScriptingParams.ConnectionString property.</target>
<note></note>
</trans-unit>
<trans-unit id="ScriptingParams_FilePath_Property_Invalid">
<source>Invalid directory specified by the ScriptingParams.FilePath property.</source>
<target state="new">Invalid directory specified by the ScriptingParams.FilePath property.</target>
<note></note>
</trans-unit>
<trans-unit id="ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid">
<source>Error parsing ScriptingListObjectsCompleteParams.ConnectionString property.</source>
<target state="new">Error parsing ScriptingListObjectsCompleteParams.ConnectionString property.</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,28 @@
//
// 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
{
/// <summary>
/// Parameters to cancel a scripting request.
/// </summary>
public class ScriptingCancelParams : ScriptingEventParams { }
/// <summary>
/// Parameters returned from a scripting request.
/// </summary>
public class ScriptingCancelResult { }
/// <summary>
/// Defines the scripting cancel request type.
/// </summary>
public class ScriptingCancelRequest
{
public static readonly RequestType<ScriptingCancelParams, ScriptingCancelResult> Type =
RequestType<ScriptingCancelParams, ScriptingCancelResult>.Create("scripting/scriptCancel");
}
}

View File

@@ -0,0 +1,34 @@
//
// 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
{
/// <summary>
/// Parameters sent to when a scripting operation has completed.
/// </summary>
public class ScriptingCompleteParams : ScriptingEventParams
{
public string ErrorDetails { get; set; }
public string ErrorMessage { get; set; }
public bool HasError { get; set; }
public bool Canceled { get; set; }
public bool Success { get; set; }
}
/// <summary>
/// Event sent to indicate a scripting operation has completed.
/// </summary>
public class ScriptingCompleteEvent
{
public static readonly EventType<ScriptingCompleteParams> Type =
EventType<ScriptingCompleteParams>.Create("scripting/scriptComplete");
}
}

View File

@@ -0,0 +1,18 @@
//
// 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
{
/// <summary>
/// Base class for all scripting event parameters.
/// </summary>
public abstract class ScriptingEventParams
{
/// <summary>
/// Gets or sets the operation id of the scripting operation this event is associated with.
/// </summary>
public string OperationId { get; set; }
}
}

View File

@@ -0,0 +1,35 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts
{
/// <summary>
/// Parameters sent when a list objects operation has completed.
/// </summary>
public class ScriptingListObjectsCompleteParams : ScriptingCompleteParams
{
/// <summary>
/// Gets or sets the list of database objects returned from the list objects operation.
/// </summary>
public List<ScriptingObject> DatabaseObjects { get; set; }
/// <summary>
/// Gets or sets the count of database object returned from the list objects operation.
/// </summary>
public int Count { get; set; }
}
/// <summary>
/// Event sent to indicate a list objects operation has completed.
/// </summary>
public class ScriptingListObjectsCompleteEvent
{
public static readonly EventType<ScriptingListObjectsCompleteParams> Type =
EventType<ScriptingListObjectsCompleteParams>.Create("scripting/listObjectsComplete");
}
}

View File

@@ -0,0 +1,35 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts
{
/// <summary>
/// Parameters for a list objects request.
/// </summary>
public class ScriptingListObjectsParams
{
public string ConnectionString { get; set; }
}
/// <summary>
/// Parameters returned from a list objects request.
/// </summary>
public class ScriptingListObjectsResult
{
public string OperationId { get; set; }
}
/// <summary>
/// Defines the scripting list objects request type.
/// </summary>
public class ScriptingListObjectsRequest
{
public static readonly RequestType<ScriptingListObjectsParams, ScriptingListObjectsResult> Type =
RequestType<ScriptingListObjectsParams, ScriptingListObjectsResult>.Create("scripting/listObjects");
}
}

View File

@@ -0,0 +1,93 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts
{
/// <summary>
/// Class to represent a database object that can be scripted.
/// </summary>
public sealed class ScriptingObject : IEquatable<ScriptingObject>
{
/// <summary>
/// Gets or sets the database object type.
/// </summary>
/// <remarks>
/// This underlying values are determined by the SqlScriptPublishModel.GetDatabaseObjectTypes() and
/// can change depending on the version of SMO used by the tools service. Values can be:
/// Table,
/// View,
/// StoredProcedure,
/// UserDefinedFunction,
/// UserDefinedDataType,
/// User,
/// Default,
/// Rule,
/// DatabaseRole,
/// ApplicationRole,
/// SqlAssembly,
/// DdlTrigger,
/// Synonym,
/// XmlSchemaCollection,
/// Schema,
/// PlanGuide,
/// UserDefinedType,
/// UserDefinedAggregate,
/// FullTextCatalog,
/// UserDefinedTableType
/// </remarks>
public string Type { get; set; }
/// <summary>
/// Gets or sets the schema of the database object.
/// </summary>
public string Schema { get; set; }
/// <summary>
/// Gets or sets the database object name.
/// </summary>
public string Name { get; set; }
public override string ToString()
{
string objectName = string.IsNullOrEmpty(this.Schema)
? this.Name
: this.Schema + "." + this.Name;
return objectName;
}
public override int GetHashCode()
{
return
StringComparer.OrdinalIgnoreCase.GetHashCode(this.Type ?? string.Empty) ^
StringComparer.OrdinalIgnoreCase.GetHashCode(this.Schema ?? string.Empty) ^
StringComparer.OrdinalIgnoreCase.GetHashCode(this.Name ?? string.Empty);
}
public override bool Equals(object obj)
{
return
obj != null &&
this.GetType() == obj.GetType() &&
this.Equals((ScriptingObject)obj);
}
public bool Equals(ScriptingObject other)
{
if (other == null)
{
return false;
}
return
string.Equals(this.Type, other.Type, StringComparison.OrdinalIgnoreCase) &&
string.Equals(this.Schema, other.Schema, StringComparison.OrdinalIgnoreCase) &&
string.Equals(this.Name, other.Name, StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,258 @@
//
// 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
{
/// <summary>
/// Defines the scripting options.
/// </summary>
public class ScriptOptions
{
/// <summary>
/// Generate ANSI padding statements
/// </summary>
public bool? ScriptAnsiPadding { get; set; } = false;
/// <summary>
/// Append the generated script to a file
/// </summary>
public bool? AppendToFile { get; set; } = false;
/// <summary>
/// Continue to script if an error occurs. Otherwise, stop.
/// </summary>
/// <remarks>
/// The default is true.
/// </remarks>
public bool? ContinueScriptingOnError { get; set; } = true;
/// <summary>
/// Convert user-defined data types to base types.
/// </summary>
public bool? ConvertUDDTToBaseType { get; set; } = false;
/// <summary>
/// Generate script for dependent objects for each object scripted.
/// </summary>
/// <remarks>
/// The default is false.
/// </remarks>
public bool? GenerateScriptForDependentObjects { get; set; } = false;
/// <summary>
/// Include descriptive headers for each object generated.
/// </summary>
/// <remarks>
/// The default is true.
/// </remarks>
public bool? IncludeDescriptiveHeaders { get; set; } = true;
/// <summary>
/// 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.
/// </summary>
public bool? IncludeIfNotExists { get; set; } = false;
/// <summary>
/// Script options to set vardecimal storage format.
/// </summary>
public bool? IncludeVarDecimal { get; set; } = true;
/// <summary>
/// Include system generated constraint names to enforce declarative referential integrity.
/// </summary>
public bool? ScriptDriIncludeSystemNames { get; set; } = false;
/// <summary>
/// Include statements in the script that are not supported on the specified SQL Server database engine type.
/// </summary>
public bool? IncludeUnsupportedStatements { get; set; } = true;
/// <summary>
/// Prefix object names with the object schema.
/// </summary>
/// <remarks>
/// The default is true.
/// </remarks>
public bool? SchemaQualify { get; set; } = true;
/// <summary>
/// Script options to set bindings option.
/// </summary>
public bool? Bindings { get; set; } = false;
/// <summary>
/// Script the objects that use collation.
/// </summary>
public bool? Collation { get; set; } = false;
/// <summary>
/// Script the default values.
/// </summary>
/// <remarks>
/// The default is true.
/// </remarks>
public bool? Default { get; set; } = true;
/// <summary>
/// Script Object CREATE/DROP statements.
/// Possible values:
/// ScriptCreate
/// ScriptDrop
/// ScriptCreateDrop
/// </summary>
/// <remarks>
/// The default is ScriptCreate.
/// </remarks>
public string ScriptCreateDrop { get; set; } = "ScriptCreate";
/// <summary>
/// Script the Extended Properties for each object scripted.
/// </summary>
/// <remarks>
/// The default is true.
/// </remarks>
public bool? ScriptExtendedProperties { get; set; } = true;
/// <summary>
/// Script only features compatible with the specified version of SQL Server. Possible values:
/// Script90Compat
/// Script100Compat
/// Script105Compat
/// Script110Compat
/// Script120Compat
/// Script130Compat
/// Script140Compat
/// </summary>
/// <remarks>
/// The default is Script140Compat.
/// </remarks>
public string ScriptCompatibilityOption { get; set; } = "Script140Compat";
/// <summary>
/// Script only features compatible with the specified SQL Server database engine edition.
/// Possible Values:
/// SqlServerPersonalEdition
/// SqlServerStandardEdition
/// SqlServerEnterpriseEdition
/// SqlServerExpressEdition
/// SqlAzureDatabaseEdition
/// SqlDatawarehouseEdition
/// SqlServerStretchEdition
/// </summary>
public string TargetDatabaseEngineEdition { get; set; } = "SqlServerEnterpriseEdition";
/// <summary>
/// Script only features compatible with the specified SQL Server database engine type.
/// Possible Values:
/// SingleInstance
/// SqlAzure
/// </summary>
public string TargetDatabaseEngineType { get; set; } = "SingleInstance";
/// <summary>
/// Script all logins available on the server. Passwords will not be scripted.
/// </summary>
public bool? ScriptLogins { get; set; } = false;
/// <summary>
/// Generate object-level permissions.
/// </summary>
public bool? ScriptObjectLevelPermissions { get; set; } = false;
/// <summary>
/// Script owner for the objects.
/// </summary>
public bool? ScriptOwner { get; set; } = false;
/// <summary>
/// Script statistics, and optionally include histograms, for each selected table or view.
/// Possible values:
/// ScriptStatsNone
/// ScriptStatsDDL
/// ScriptStatsAll
/// </summary>
/// <remarks>
/// The default value is ScriptStatsNone.
/// </remarks>
public string ScriptStatistics { get; set; } = "ScriptStatsNone";
/// <summary>
/// Generate USE DATABASE statement.
/// </summary>
public bool? ScriptUseDatabase { get; set; } = true;
/// <summary>
/// Generate script that contains schema only or schema and data.
/// Possible Values:
/// SchemaAndData
/// DataOnly
/// SchemaOnly
/// </summary>
/// <remarks>
/// The default value is SchemaOnly.
/// </remarks>
public string TypeOfDataToScript { get; set; } = "SchemaOnly";
/// <summary>
/// Scripts the change tracking information.
/// </summary>
public bool? ScriptChangeTracking { get; set; } = false;
/// <summary>
/// Script the check constraints for each table or view scripted.
/// </summary>
/// <remarks>
/// The default value is true.
/// </remarks>
public bool? ScriptCheckConstraints { get; set; } = true;
/// <summary>
/// Scripts the data compression information.
/// </summary>
public bool? ScriptDataCompressionOptions { get; set; } = false;
/// <summary>
/// Script the foreign keys for each table scripted.
/// </summary>
/// <remarks>
/// The default value is true.
/// </remarks>
public bool? ScriptForeignKeys { get; set; } = true;
/// <summary>
/// Script the full-text indexes for each table or indexed view scripted.
/// </summary>
public bool? ScriptFullTextIndexes { get; set; } = false;
/// <summary>
/// Script the indexes (including XML and clustered indexes) for each table or indexed view scripted.
/// </summary>
/// <remarks>
/// The default value is true.
/// </remarks>
public bool? ScriptIndexes { get; set; } = true;
/// <summary>
/// Script the primary keys for each table or view scripted
/// </summary>
/// <remarks>
/// The default value is true.
/// </remarks>
public bool? ScriptPrimaryKeys { get; set; } = true;
/// <summary>
/// Script the triggers for each table or view scripted
/// </summary>
public bool? ScriptTriggers { get; set; } = false;
/// <summary>
/// Script the unique keys for each table or view scripted.
/// </summary>
/// <remarks>
/// The default value is true.
/// </remarks>
public bool? UniqueKeys { get; set; } = true;
}
}

View File

@@ -0,0 +1,34 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts
{
/// <summary>
/// Parameters to indicate the script operation has resolved the objects to be scripted.
/// </summary>
public class ScriptingPlanNotificationParams : ScriptingEventParams
{
/// <summary>
/// Gets or sets the list of database objects whose progress has changed.
/// </summary>
public List<ScriptingObject> ScriptingObjects { get; set; }
/// <summary>
/// Gets or sets the count of database objects whose progress has changed.
/// </summary>
public int Count { get; set; }
}
/// <summary>
/// Event sent to indicate a script operation has determined which objects will be scripted.
/// </summary>
public class ScriptingPlanNotificationEvent
{
public static readonly EventType<ScriptingPlanNotificationParams> Type = EventType<ScriptingPlanNotificationParams>.Create("scripting/scriptPlanNotification");
}
}

View File

@@ -0,0 +1,52 @@
//
// 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
{
/// <summary>
/// Parameters sent when a scripting operation has made progress.
/// </summary>
public class ScriptingProgressNotificationParams : ScriptingEventParams
{
/// <summary>
/// Gets or sets the scripting object whose progress has changed.
/// </summary>
public ScriptingObject ScriptingObject { get; set; }
/// <summary>
/// Gets or sets the status of the scripting operation for the scripting object.
/// </summary>
/// <remarks>
/// Values can be: 'Completed', 'Progress', and 'Error'.
/// </remarks>
public string Status { get; set; }
/// <summary>
/// Gets or count of completed scripting operations.
/// </summary>
public int CompletedCount { get; set; }
/// <summary>
/// Gets this total count of objects to script.
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Gets or sets the error details if an error occurred scripting a database object.
/// </summary>
public string ErrorDetails { get; set; }
}
/// <summary>
/// Event to indicate the scripting operation has made progress.
/// </summary>
public class ScriptingProgressNotificationEvent
{
public static readonly EventType<ScriptingProgressNotificationParams> Type =
EventType<ScriptingProgressNotificationParams>.Create("scripting/scriptProgressNotification");
}
}

View File

@@ -0,0 +1,63 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts
{
/// <summary>
/// Parameters for a script request.
/// </summary>
public class ScriptingParams
{
/// <summary>
/// Gets or sets the file path used when writing out the script.
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// Gets or sets connection string of the target database the scripting operation will run against.
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// Gets or sets a list of scripting objects to script.
/// </summary>
public List<ScriptingObject> ScriptingObjects { get; set; }
/// <summary>
/// Gets or sets a list of scripting object which specify the include criteria of objects to script.
/// </summary>
public List<ScriptingObject> IncludeObjectCriteria { get; set; }
/// <summary>
/// Gets or sets a list of scripting object which specify the exclude criteria of objects to not script.
/// </summary>
public List<ScriptingObject> ExcludeObjectCriteria { get; set; }
/// <summary>
/// Gets or sets the scripting options.
/// </summary>
public ScriptOptions ScriptOptions { get; set; }
}
/// <summary>
/// Parameters returned from a script request.
/// </summary>
public class ScriptingResult
{
public string OperationId { get; set; }
}
/// <summary>
/// Defines the scripting request type.
/// </summary>
public class ScriptingRequest
{
public static readonly RequestType<ScriptingParams, ScriptingResult> Type =
RequestType<ScriptingParams, ScriptingResult>.Create("scripting/script");
}
}

View File

@@ -0,0 +1,101 @@
//
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.SqlScriptPublish;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Extension methods used by the scripting service.
/// </summary>
internal static class ScriptingExtensionMethods
{
/// <summary>
/// Returns a list of ScriptingObject instances for the passed SqlScriptPublishModel instance.
/// </summary>
/// <param name="publishModel">The sql script publish model instance.</param>
/// <returns>The list of scripting objects.</returns>
public static List<ScriptingObject> GetDatabaseObjects(this SqlScriptPublishModel publishModel)
{
Validate.IsNotNull("publishModel", publishModel);
List<ScriptingObject> databaseObjects = new List<ScriptingObject>();
IEnumerable<DatabaseObjectType> objectTypes = publishModel.GetDatabaseObjectTypes();
Logger.Write(
LogLevel.Verbose,
string.Format(
"Loaded SMO object type count {0}, types: {1}",
objectTypes.Count(),
string.Join(", ", objectTypes)));
foreach (DatabaseObjectType objectType in objectTypes)
{
IEnumerable<KeyValuePair<string, string>> databaseObjectsOfType = publishModel.EnumChildrenForDatabaseObjectType(objectType);
Logger.Write(
LogLevel.Verbose,
string.Format(
"Loaded SMO urn object count {0} for type {1}, urns: {2}",
objectType,
databaseObjectsOfType.Count(),
string.Join(", ", databaseObjectsOfType.Select(p => p.Value))));
databaseObjects.AddRange(databaseObjectsOfType.Select(d => new Urn(d.Value).ToScriptingObject()));
}
return databaseObjects;
}
/// <summary>
/// Creates a SMO Urn instance based on the passed ScriptingObject instance.
/// </summary>
/// <param name="scriptingObject">The scripting object instance.</param>
/// <param name="database">The name of the database referenced by the Urn.</param>
/// <returns>The Urn instance.</returns>
public static Urn ToUrn(this ScriptingObject scriptingObject, string database)
{
Validate.IsNotNull("scriptingObject", scriptingObject);
Validate.IsNotNullOrWhitespaceString("database", database);
Validate.IsNotNullOrWhitespaceString("scriptingObject.Name", scriptingObject.Name);
Validate.IsNotNullOrWhitespaceString("scriptingObject.Type", scriptingObject.Type);
// Leaving the server name blank will automatically match whatever the server SMO is running against.
string urn = string.Format(
"Server/Database[@Name='{0}']/{1}[@Name='{2}' {3}]",
database,
scriptingObject.Type,
scriptingObject.Name,
scriptingObject.Schema != null ? string.Format("and @Schema = '{0}'", scriptingObject.Schema) : string.Empty);
return new Urn(urn);
}
/// <summary>
/// Creates a ScriptingObject instance based on the passed SMO Urn instance.
/// </summary>
/// <param name="urn">The urn instance.</param>
/// <returns>The scripting object instance.</returns>
public static ScriptingObject ToScriptingObject(this Urn urn)
{
Validate.IsNotNull("urn", urn);
return new ScriptingObject
{
Type = urn.Type,
Schema = urn.GetAttribute("Schema"),
Name = urn.GetAttribute("Name"),
};
}
}
}

View File

@@ -0,0 +1,132 @@
//
// 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 System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.SqlScriptPublish;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Class to represent an in-progress list objects operation.
/// </summary>
public sealed class ScriptingListObjectsOperation : ScriptingOperation
{
private bool disposed = false;
public ScriptingListObjectsOperation(ScriptingListObjectsParams parameters)
{
Validate.IsNotNull("parameters", parameters);
this.Parameters = parameters;
}
private ScriptingListObjectsParams Parameters { get; set; }
/// <summary>
/// Event raised when a the list object operation is complete.
/// </summary>
/// <remarks>
/// An event can be completed by the following conditions: success, cancel, error.
/// </remarks>
public event EventHandler<ScriptingListObjectsCompleteParams> CompleteNotification;
public override void Execute()
{
if (this.CancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(this.CancellationToken);
}
SqlScriptPublishModel publishModel = null;
try
{
this.ValidateScriptDatabaseParams();
publishModel = new SqlScriptPublishModel(this.Parameters.ConnectionString);
List<ScriptingObject> databaseObjects = publishModel.GetDatabaseObjects();
Logger.Write(
LogLevel.Verbose,
string.Format(
"Sending list object completion notification count {0}, objects: {1}",
databaseObjects,
string.Join(", ", databaseObjects)));
this.SendCompletionNotificationEvent(new ScriptingListObjectsCompleteParams
{
OperationId = this.OperationId,
DatabaseObjects = databaseObjects,
Count = databaseObjects.Count,
Success = true,
});
}
catch (Exception e)
{
Logger.Write(LogLevel.Normal, string.Format("Scripting operation {0} was canceled", this.OperationId));
if (e.IsOperationCanceledException())
{
this.SendCompletionNotificationEvent(new ScriptingListObjectsCompleteParams
{
OperationId = this.OperationId,
Canceled = true,
});
}
else
{
Logger.Write(LogLevel.Error, string.Format("Scripting operation {0} failed with exception {1}", this.OperationId, e));
this.SendCompletionNotificationEvent(new ScriptingListObjectsCompleteParams
{
OperationId = this.OperationId,
HasError = true,
ErrorMessage = e.Message,
ErrorDetails = e.ToString(),
});
}
}
}
private void SendCompletionNotificationEvent(ScriptingListObjectsCompleteParams parameters)
{
this.CompleteNotification?.Invoke(this, parameters);
}
private void ValidateScriptDatabaseParams()
{
try
{
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(this.Parameters.ConnectionString);
}
catch (Exception e)
{
throw new ArgumentException(SR.ScriptingListObjectsCompleteParams_ConnectionString_Property_Invalid, e);
}
}
/// <summary>
/// Disposes the scripting operation.
/// </summary>
public override void Dispose()
{
if (!disposed)
{
this.Cancel();
disposed = true;
}
}
}
}

View File

@@ -0,0 +1,153 @@
//
// 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.Linq;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Implements matching logic to filter scripting objects based on an
/// include/exclude criteria.
/// </summary>
/// <remarks>
/// First, objects are included by the include filter. Then, objects are removed by
/// the exclude filter. Matches are made by comparing case insensitive strings for the
/// ScriptingObject Type, Schema, and Name properties. Wildcards '*' are supported for
/// the ScriptingObject Schema and Name properties. Matching on ScriptingObject Type
/// property must be an exact match.
///
/// Examples:
///
/// Include ScriptingObject { Type = null, Schema = "dbo", Name = null }
/// -> matches all objects in the dbo schema.
///
/// Include ScriptingObject { Type = "Table", Schema = "dbo", Name = null }
/// -> matches all tables in the dbo schema.
///
/// Include ScriptingObject { Type = "Table", Schema = null, Name = "Emp*" }
/// -> matches all table names that start with "Emp"
///
/// Include ScriptingObject { Type = "View", Schema = null, Name = "Emp*" }
/// Include ScriptingObject { Type = "Table", Schema = null, Name = "Emp*" }
/// -> matches all table and views with names that start with "Emp"
///
/// Include ScriptingObject { Type = "Table", Schema = null, Name = null }
/// Exclude ScriptingObject { Type = null, Schema = "HumanResources", Name = null }
/// -> matches all tables except tables in the "HumanResources" schema
///
/// </remarks>
public static class ScriptingObjectMatcher
{
private const string Wildcard = "*";
/// <summary>
/// Given a collection of candidate scripting objects, filters the items that match
/// based on the passed include and exclude criteria.
/// </summary>
/// <param name="includeCriteria">The include object criteria.</param>
/// <param name="excludeCriteria">The exclude object criteria.</param>
/// <param name="candidates">The candidate object to filter.</param>
/// <returns>The matching scripting objects.</returns>
public static IEnumerable<ScriptingObject> Match(
ScriptingObject includeCriteria,
ScriptingObject excludeCriteria,
IEnumerable<ScriptingObject> candidates)
{
return Match(
includeCriteria == null ? new ScriptingObject[0] : new[] { includeCriteria },
excludeCriteria == null ? new ScriptingObject[0] : new[] { excludeCriteria },
candidates);
}
/// <summary>
/// Given a collection of candidate scripting objects, filters the items that match
/// based on the passed include and exclude criteria.
/// </summary>
/// <param name="includeCriteria">The collection of include object criteria items.</param>
/// <param name="excludeCriteria">The collection of exclude object criteria items.</param>
/// <param name="candidates">The candidate object to filter.</param>
/// <returns>The matching scripting objects.</returns>
public static IEnumerable<ScriptingObject> Match(
IEnumerable<ScriptingObject> includeCriteria,
IEnumerable<ScriptingObject> excludeCriteria,
IEnumerable<ScriptingObject> candidates)
{
Validate.IsNotNull("candidates", candidates);
IEnumerable<ScriptingObject> matchedObjects = new List<ScriptingObject>();
if (includeCriteria != null && includeCriteria.Any())
{
foreach (ScriptingObject scriptingObjectCriteria in includeCriteria)
{
IEnumerable<ScriptingObject> matches = MatchCriteria(scriptingObjectCriteria, candidates);
matchedObjects = matchedObjects.Union(matches);
}
}
else
{
matchedObjects = candidates;
}
if (excludeCriteria != null)
{
foreach (ScriptingObject scriptingObjectCriteria in excludeCriteria)
{
IEnumerable<ScriptingObject> matches = MatchCriteria(scriptingObjectCriteria, candidates);
matchedObjects = matchedObjects.Except(matches);
}
}
return matchedObjects;
}
private static IEnumerable<ScriptingObject> MatchCriteria(ScriptingObject criteria, IEnumerable<ScriptingObject> candidates)
{
Validate.IsNotNull("criteria", criteria);
Validate.IsNotNull("candidates", candidates);
IEnumerable<ScriptingObject> matchedObjects = candidates;
if (!string.IsNullOrWhiteSpace(criteria.Type))
{
matchedObjects = matchedObjects.Where(o => string.Equals(criteria.Type, o.Type, StringComparison.OrdinalIgnoreCase));
}
matchedObjects = MatchCriteria(criteria.Schema, (candidate) => { return candidate.Schema; }, matchedObjects);
matchedObjects = MatchCriteria(criteria.Name, (candidate) => { return candidate.Name; }, matchedObjects);
return matchedObjects;
}
private static IEnumerable<ScriptingObject> MatchCriteria(string property, Func<ScriptingObject, string> propertySelector, IEnumerable<ScriptingObject> candidates)
{
IEnumerable<ScriptingObject> matchedObjects = candidates;
if (!string.IsNullOrWhiteSpace(property))
{
if (property.Equals(Wildcard, StringComparison.OrdinalIgnoreCase))
{
// Don't filter any objects
}
if (property.EndsWith(Wildcard, StringComparison.OrdinalIgnoreCase))
{
matchedObjects = candidates.Where(o => propertySelector(o).StartsWith(
propertySelector(o).Substring(0, propertySelector(o).Length - 1),
StringComparison.OrdinalIgnoreCase));
}
else
{
matchedObjects = matchedObjects.Where(o => string.Equals(property, propertySelector(o), StringComparison.OrdinalIgnoreCase));
}
}
return matchedObjects;
}
}
}

View File

@@ -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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Base class for scripting operations. Because scripting operations can be very long
/// running, there my be multiple concurrent scripting operations. To distinguish events
/// between concurrent scripting operations, use the operation id.
/// </summary>
public abstract class ScriptingOperation : IDisposable
{
private CancellationTokenSource cancellation = new CancellationTokenSource();
protected ScriptingOperation()
{
this.OperationId = Guid.NewGuid().ToString();
}
protected CancellationToken CancellationToken { get { return this.cancellation.Token; } }
/// <summary>
/// Gets the unique id associated with this instance.
/// </summary>
public string OperationId { get; private set; }
/// <summary>
/// Excecutes the scripting operation.
/// </summary>
public abstract void Execute();
/// <summary>
/// Cancels the scripting operation.
/// </summary>
public virtual void Cancel()
{
if (!this.cancellation.IsCancellationRequested)
{
Logger.Write(LogLevel.Verbose, string.Format("Cancel invoked for OperationId {0}", this.OperationId));
this.cancellation.Cancel();
}
}
/// <summary>
/// Disposes the scripting operation.
/// </summary>
public abstract void Dispose();
}
}

View File

@@ -0,0 +1,366 @@
//
// 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 System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Sdk.Sfc;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.SqlScriptPublish;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using static Microsoft.SqlServer.Management.SqlScriptPublish.SqlScriptOptions;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Class to represent an in-progress script operation.
/// </summary>
public sealed class ScriptingScriptOperation : ScriptingOperation
{
private bool disposed = false;
private int scriptedObjectCount = 0;
private int totalScriptedObjectCount = 0;
public ScriptingScriptOperation(ScriptingParams parameters)
{
Validate.IsNotNull("parameters", parameters);
this.Parameters = parameters;
}
private ScriptingParams Parameters { get; set; }
/// <summary>
/// Event raised when a scripting operation has resolved which database objects will be scripted.
/// </summary>
public event EventHandler<ScriptingPlanNotificationParams> PlanNotification;
/// <summary>
/// Event raised when a scripting operation has made forward progress.
/// </summary>
public event EventHandler<ScriptingProgressNotificationParams> ProgressNotification;
/// <summary>
/// Event raised when a scripting operation is complete.
/// </summary>
/// <remarks>
/// An event can be completed by the following conditions: success, cancel, error.
/// </remarks>
public event EventHandler<ScriptingCompleteParams> CompleteNotification;
public override void Execute()
{
SqlScriptPublishModel publishModel = null;
try
{
this.CancellationToken.ThrowIfCancellationRequested();
this.ValidateScriptDatabaseParams();
publishModel = BuildPublishModel();
publishModel.ScriptItemsCollected += this.OnPublishModelScriptItemsCollected;
publishModel.ScriptProgress += this.OnPublishModelScriptProgress;
publishModel.ScriptError += this.OnPublishModelScriptError;
ScriptOutputOptions outputOptions = new ScriptOutputOptions
{
SaveFileMode = ScriptFileMode.Overwrite,
SaveFileType = ScriptFileType.Unicode, // UTF-16
SaveFileName = this.Parameters.FilePath,
};
this.CancellationToken.ThrowIfCancellationRequested();
publishModel.GenerateScript(outputOptions);
this.CancellationToken.ThrowIfCancellationRequested();
Logger.Write(
LogLevel.Verbose,
string.Format(
"Sending script complete notification event with total count {0} and scripted count {1}",
this.totalScriptedObjectCount,
this.scriptedObjectCount));
this.SendCompletionNotificationEvent(new ScriptingCompleteParams
{
OperationId = this.OperationId,
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
{
OperationId = this.OperationId,
Canceled = true,
});
}
else
{
Logger.Write(LogLevel.Error, string.Format("Scripting operation {0} failed with exception {1}", this.OperationId, e));
this.SendCompletionNotificationEvent(new ScriptingCompleteParams
{
OperationId = this.OperationId,
HasError = true,
ErrorMessage = e.Message,
ErrorDetails = e.ToString(),
});
}
}
finally
{
if (publishModel != null)
{
publishModel.ScriptItemsCollected -= this.OnPublishModelScriptItemsCollected;
publishModel.ScriptProgress -= this.OnPublishModelScriptProgress;
publishModel.ScriptError -= this.OnPublishModelScriptError;
}
}
}
private void SendCompletionNotificationEvent(ScriptingCompleteParams parameters)
{
this.CompleteNotification?.Invoke(this, parameters);
}
private void SendPlanNotificationEvent(ScriptingPlanNotificationParams parameters)
{
this.PlanNotification?.Invoke(this, parameters);
}
private void SendProgressNotificationEvent(ScriptingProgressNotificationParams parameters)
{
this.ProgressNotification?.Invoke(this, parameters);
}
private SqlScriptPublishModel BuildPublishModel()
{
SqlScriptPublishModel publishModel = new SqlScriptPublishModel(this.Parameters.ConnectionString);
PopulateAdvancedScriptOptions(publishModel.AdvancedOptions);
bool hasIncludeCriteria = this.Parameters.IncludeObjectCriteria != null && this.Parameters.IncludeObjectCriteria.Any();
bool hasExcludeCriteria = this.Parameters.ExcludeObjectCriteria != null && this.Parameters.ExcludeObjectCriteria.Any();
bool hasObjectsSpecified = this.Parameters.ScriptingObjects != null && this.Parameters.ScriptingObjects.Any();
// If no object selection criteria was specified, we're scripting the entire database
publishModel.ScriptAllObjects = !(hasIncludeCriteria || hasExcludeCriteria || hasObjectsSpecified);
if (publishModel.ScriptAllObjects)
{
publishModel.AdvancedOptions.GenerateScriptForDependentObjects = BooleanTypeOptions.True;
return publishModel;
}
// An object selection criteria was specified, so now we need to resolve the SMO Urn instances to script.
IEnumerable<ScriptingObject> selectedObjects = new List<ScriptingObject>();
if (hasIncludeCriteria || hasExcludeCriteria)
{
// This is an expensive remote call to load all objects from the database.
List<ScriptingObject> allObjects = publishModel.GetDatabaseObjects();
selectedObjects = ScriptingObjectMatcher.Match(
this.Parameters.IncludeObjectCriteria,
this.Parameters.ExcludeObjectCriteria,
allObjects);
}
// If specific objects are specified, include them.
if (hasObjectsSpecified)
{
selectedObjects = selectedObjects.Union(this.Parameters.ScriptingObjects);
}
Logger.Write(
LogLevel.Normal,
string.Format(
"Scripting object count {0}, objects: {1}",
selectedObjects.Count(),
string.Join(", ", selectedObjects)));
string database = new SqlConnectionStringBuilder(this.Parameters.ConnectionString).InitialCatalog;
foreach (ScriptingObject scriptingObject in selectedObjects)
{
publishModel.SelectedObjects.Add(scriptingObject.ToUrn(database));
}
return publishModel;
}
private void PopulateAdvancedScriptOptions(SqlScriptOptions advancedOptions)
{
foreach (PropertyInfo optionPropInfo in this.Parameters.ScriptOptions.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(this.Parameters.ScriptOptions, index: null);
if (optionValue == null)
{
#if DEBUG
Logger.Write(LogLevel.Verbose, string.Format("Skipping ScriptOptions.{0} since value is null", optionPropInfo.Name));
#endif
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);
}
#if DEBUG
Logger.Write(LogLevel.Verbose, string.Format("Setting ScriptOptions.{0} to value {1}", optionPropInfo.Name, smoValue));
#endif
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 (!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();
Logger.Write(
LogLevel.Verbose,
string.Format(
"Sending scripting error progress event, Urn={0}, OperationId={1}, Completed={2}, Error={3}",
e.Urn,
this.OperationId,
e.Completed,
e.Error));
// Keep scripting...it's a best effort operation.
e.ContinueScripting = true;
this.SendProgressNotificationEvent(new ScriptingProgressNotificationParams
{
OperationId = this.OperationId,
ScriptingObject = e.Urn?.ToScriptingObject(),
Status = "Error",
CompletedCount = this.scriptedObjectCount,
TotalCount = this.totalScriptedObjectCount,
ErrorDetails = e?.ToString(),
});
}
private void OnPublishModelScriptItemsCollected(object sender, ScriptItemsArgs e)
{
this.CancellationToken.ThrowIfCancellationRequested();
List<ScriptingObject> scriptingObjects = e.Urns.Select(urn => urn.ToScriptingObject()).ToList();
this.totalScriptedObjectCount = scriptingObjects.Count;
Logger.Write(
LogLevel.Verbose,
string.Format(
"Sending plan notification event with count {0}, objects: {1}",
this.totalScriptedObjectCount,
string.Join(", ", e.Urns)));
this.SendPlanNotificationEvent(new ScriptingPlanNotificationParams
{
OperationId = this.OperationId,
ScriptingObjects = scriptingObjects,
Count = scriptingObjects.Count,
});
}
private void OnPublishModelScriptProgress(object sender, ScriptEventArgs e)
{
this.CancellationToken.ThrowIfCancellationRequested();
if (e.Completed)
{
this.scriptedObjectCount += 1;
}
Logger.Write(
LogLevel.Verbose,
string.Format(
"Sending progress event, Urn={0}, OperationId={1}, Completed={2}, Error={3}",
e.Urn,
this.OperationId,
e.Completed,
e.Error));
this.SendProgressNotificationEvent(new ScriptingProgressNotificationParams
{
OperationId = this.OperationId,
ScriptingObject = e.Urn.ToScriptingObject(),
Status = e.Completed ? "Completed" : "Progress",
CompletedCount = this.scriptedObjectCount,
TotalCount = this.totalScriptedObjectCount,
ErrorDetails = e?.ToString(),
});
}
/// <summary>
/// Disposes the scripting operation.
/// </summary>
public override void Dispose()
{
if (!disposed)
{
this.Cancel();
disposed = true;
}
}
}
}

View File

@@ -4,24 +4,27 @@
//
using System;
using System.Collections.Concurrent;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Main class for Scripting Service functionality
/// </summary>
public sealed class ScriptingService
public sealed class ScriptingService : IDisposable
{
private const int ScriptingOperationTimeout = 60000;
@@ -33,7 +36,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
private static LanguageService languageServices = null;
/// <summary>
private readonly Lazy<ConcurrentDictionary<string, ScriptingOperation>> operations =
new Lazy<ConcurrentDictionary<string, ScriptingOperation>>(() => new ConcurrentDictionary<string, ScriptingOperation>());
private bool disposed;
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal static ConnectionService ConnectionServiceInstance
@@ -72,6 +80,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
}
}
/// <summary>
/// The collection of active operations
/// </summary>
internal ConcurrentDictionary<string, ScriptingOperation> ActiveOperations => operations.Value;
/// <summary>
/// Initializes the Scripting Service instance
/// </summary>
@@ -80,6 +93,132 @@ 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);
// Register handler for shutdown event
serviceHost.RegisterShutdownTask((shutdownParams, requestContext) =>
{
this.Dispose();
return Task.FromResult(0);
});
}
/// <summary>
/// Handles request to execute start the list objects operation.
/// </summary>
private async Task HandleListObjectsRequest(ScriptingListObjectsParams parameters, RequestContext<ScriptingListObjectsResult> requestContext)
{
try
{
ScriptingListObjectsOperation operation = new ScriptingListObjectsOperation(parameters);
operation.CompleteNotification += (sender, e) => this.SendEvent(requestContext, ScriptingListObjectsCompleteEvent.Type, e);
RunTask(requestContext, operation);
await requestContext.SendResult(new ScriptingListObjectsResult { OperationId = operation.OperationId });
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
/// <summary>
/// Handles request to execute start the script operation.
/// </summary>
public async Task HandleScriptExecuteRequest(ScriptingParams parameters, RequestContext<ScriptingResult> requestContext)
{
try
{
ScriptingScriptOperation operation = new ScriptingScriptOperation(parameters);
operation.PlanNotification += (sender, e) => this.SendEvent(requestContext, ScriptingPlanNotificationEvent.Type, e);
operation.ProgressNotification += (sender, e) => this.SendEvent(requestContext, ScriptingProgressNotificationEvent.Type, e);
operation.CompleteNotification += (sender, e) => this.SendEvent(requestContext, ScriptingCompleteEvent.Type, e);
RunTask(requestContext, operation);
await requestContext.SendResult(new ScriptingResult { OperationId = operation.OperationId });
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
/// <summary>
/// Handles request to cancel a script operation.
/// </summary>
public async Task HandleScriptCancelRequest(ScriptingCancelParams parameters, RequestContext<ScriptingCancelResult> requestContext)
{
try
{
ScriptingOperation operation = null;
if (this.ActiveOperations.TryRemove(parameters.OperationId, out operation))
{
operation.Cancel();
}
else
{
Logger.Write(LogLevel.Normal, string.Format("Operation {0} was not found", operation.OperationId));
}
await requestContext.SendResult(new ScriptingCancelResult());
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
/// <summary>
/// Sends a JSON-RPC event.
/// </summary>
private void SendEvent<TParams>(IEventSender requestContext, EventType<TParams> eventType, TParams parameters)
{
Task.Run(async () => await requestContext.SendEvent(eventType, parameters));
}
/// <summary>
/// Runs the async task that performs the scripting operation.
/// </summary>
private void RunTask<T>(RequestContext<T> context, ScriptingOperation operation)
{
Task.Run(() =>
{
try
{
Debug.Assert(!this.ActiveOperations.ContainsKey(operation.OperationId), "Operation id must be unique");
this.ActiveOperations[operation.OperationId] = operation;
operation.Execute();
}
catch (Exception e)
{
context.SendError(e);
}
finally
{
ScriptingOperation temp;
this.ActiveOperations.TryRemove(operation.OperationId, out temp);
}
});
}
/// <summary>
/// Disposes the scripting service and all active scripting operations.
/// </summary>
public void Dispose()
{
if (!disposed)
{
foreach (ScriptingScriptOperation operation in this.ActiveOperations.Values)
{
operation.Dispose();
}
disposed = true;
}
}
/// <summary>

View File

@@ -20,7 +20,8 @@
"Newtonsoft.Json": "9.0.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.4.0-sqltools-24613-04",
"Microsoft.SqlServer.Smo": "14.0.17028",
"Microsoft.SqlServer.Smo": "140.17049.0",
"Microsoft.SqlServer.Management.SqlScriptPublishModel": "140.17049.0",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",

View File

@@ -17,7 +17,7 @@
"System.Runtime.Serialization.Primitives": "4.1.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.4.0-sqltools-24613-04",
"Microsoft.SqlServer.Smo": "14.0.17028",
"Microsoft.SqlServer.Smo": "140.17049.0",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",

View File

@@ -37,6 +37,7 @@
}
},
"runtimes": {
"win7-x64": {}
"win7-x64": {},
"win7-x86": {}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -47,6 +47,8 @@ END
public static string AdventureWorksScript { get { return AdventureWorksScriptInstance.Value; } }
public static string CreateNorthwindSchema { get { return CreateNorthwindSchemaInstance.Value; } }
private static readonly Lazy<string> CreateDatabaseObjectsQueryInstance = new Lazy<string>(() =>
{
return GetScriptFileContent(ResourceNameRefix + "CreateTestDatabaseObjects.sql");
@@ -67,6 +69,11 @@ END
return GetScriptFileContent(ResourceNameRefix + "AdventureWorks.sql");
});
private static readonly Lazy<string> CreateNorthwindSchemaInstance = new Lazy<string>(() =>
{
return GetScriptFileContent(ResourceNameRefix + "CreateNorthwindSchema.sql");
});
private static string GetScriptFileContent(string fileName)
{
string fileContent = string.Empty;

View File

@@ -8,6 +8,7 @@ using System.Globalization;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Xunit;
using System.Data.SqlClient;
namespace Microsoft.SqlTools.ServiceLayer.Test.Common
{
@@ -24,6 +25,31 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
public bool DoNotCleanupDb { get; set; }
public string ConnectionString
{
get
{
ConnectParams connectParams = TestConnectionProfileService.Instance.GetConnectionParameters(this.ServerType, this.DatabaseName);
SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder
{
DataSource = connectParams.Connection.ServerName,
InitialCatalog = connectParams.Connection.DatabaseName,
};
if (connectParams.Connection.AuthenticationType == "Integrated")
{
builder.IntegratedSecurity = true;
}
else
{
builder.UserID = connectParams.Connection.UserName;
builder.Password = connectParams.Connection.Password;
}
return builder.ToString();
}
}
/// <summary>
/// Create the test db if not already exists
/// </summary>
@@ -63,6 +89,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
return CreateNew(serverType, false, null, query);
}
/// <summary>
/// Create the test db if not already exists
/// </summary>
public static SqlTestDb CreateNew(TestServerType serverType)
{
return CreateNew(serverType, false, null, null);
}
/// <summary>
/// Returns a mangled name that unique based on Prefix + Machine + Process
/// </summary>
@@ -115,6 +149,16 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
return connInfo;
}
/// <summary>
/// Runs the passed query against the test db.
/// </summary>
/// <param name="query">The query to execute.</param>
/// <param name="throwOnError">If true, throw an exception if the query encounters an error executing a batch statement.</param>
public void RunQuery(string query, bool throwOnError = false)
{
TestServiceProvider.Instance.RunQuery(this.ServerType, this.DatabaseName, query, throwOnError);
}
public void Dispose()
{
Cleanup();

View File

@@ -6,10 +6,12 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.TestDriver.Driver;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
@@ -422,6 +424,21 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
return result;
}
public async Task<ScriptingListObjectsResult> ListScriptingObjects(ScriptingListObjectsParams parameters)
{
return await Driver.SendRequest(ScriptingListObjectsRequest.Type, parameters);
}
public async Task<ScriptingResult> Script(ScriptingParams parameters)
{
return await Driver.SendRequest(ScriptingRequest.Type, parameters);
}
public async Task<ScriptingCancelResult> CancelScript(string operationId)
{
return await Driver.SendRequest(ScriptingCancelRequest.Type, new ScriptingCancelParams { OperationId = operationId });
}
/// <summary>
/// Waits for a message to be returned by the service
/// </summary>
@@ -438,5 +455,30 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
System.IO.File.WriteAllText(ownerUri, query);
}
}
public bool TryGetEvent<T>(EventType<T> eventType, out T value)
{
value = default(T);
try
{
Task<T> t = this.Driver.WaitForEvent(eventType, TimeSpan.Zero);
value = t.Result;
return true;
}
catch (Exception)
{
return false;
}
}
public void AssertEventNotQueued<T>(EventType<T> eventType)
{
T temp;
if (TryGetEvent(eventType, out temp))
{
Assert.True(false, string.Format("Event of type {0} was found in the queue.", eventType.GetType().FullName, temp.ToString()));
}
}
}
}

View File

@@ -4,6 +4,8 @@
//
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SqlTools.Credentials;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
@@ -74,7 +76,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
/// <summary>
/// Runs a query by calling the services directly (not using the test driver)
/// </summary>
public void RunQuery(TestServerType serverType, string databaseName, string queryText)
public void RunQuery(TestServerType serverType, string databaseName, string queryText, bool throwOnError = false)
{
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
{
@@ -82,6 +84,18 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
Query query = new Query(queryText, connInfo, new QueryExecutionSettings(), MemoryFileSystem.GetFileStreamFactory());
query.Execute();
query.ExecutionTask.Wait();
if (throwOnError)
{
IEnumerable<Batch> errorBatches = query.Batches.Where(b => b.HasError);
if (errorBatches.Count() > 0)
{
throw new InvalidOperationException(
string.Format(
"The query encountered and error. The batches with errors: {0}",
string.Join(Environment.NewLine, errorBatches.Select(b => b.BatchText))));
}
}
}
}

View File

@@ -6,8 +6,9 @@
"includeFiles": [
"Scripts/CreateTestDatabaseObjects.sql",
"Scripts/CreateTestDatabase.sql",
"Scripts/TestDbTableQueries.sql",
"Scripts/AdventureWorks.sql"
"Scripts/AdventureWorks.sql",
"Scripts/CreateNorthwindSchema.sql",
"Scripts/TestDbTableQueries.sql"
]
}
},
@@ -17,7 +18,7 @@
"System.Runtime.Serialization.Primitives": "4.1.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.4.0-sqltools-24613-04",
"Microsoft.SqlServer.Smo": "14.0.17028",
"Microsoft.SqlServer.Smo": "140.17049.0",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",

View File

@@ -50,18 +50,25 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests
}
else
{
if (string.IsNullOrEmpty(methodName))
try
{
var methods = type.GetMethods().Where(x => x.CustomAttributes.Any(a => a.AttributeType == typeof(FactAttribute)));
foreach (var method in methods)
if (string.IsNullOrEmpty(methodName))
{
await RunTest(type, method, method.Name);
var methods = type.GetMethods().Where(x => x.CustomAttributes.Any(a => a.AttributeType == typeof(FactAttribute)));
foreach (var method in methods)
{
await RunTest(type, method, method.Name);
}
}
else
{
MethodInfo methodInfo = type.GetMethod(methodName);
await RunTest(type, methodInfo, test);
}
}
else
finally
{
MethodInfo methodInfo = type.GetMethod(methodName);
await RunTest(type, methodInfo, test);
RunTestCleanup(type);
}
}
}
@@ -92,5 +99,24 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests
}
}
}
private static void RunTestCleanup(Type type)
{
try
{
MethodInfo cleanupMethod = type.GetMethod("Cleanup");
if (cleanupMethod != null)
{
cleanupMethod.Invoke(null, null);
}
}
catch (Exception e)
{
Console.WriteLine(
"An exception occurred running Cleanup for type {0}: {1}",
type.FullName,
e);
}
}
}
}

View File

@@ -0,0 +1,340 @@
//
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.ServiceLayer.Test.Common;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Tests
{
/// <summary>
/// Scripting service end-to-end integration tests that use the SqlScriptPublishModel type to generate scripts.
/// </summary>
public class SqlScriptPublishModelTests : IClassFixture<SqlScriptPublishModelTests.ScriptingFixture>
{
public SqlScriptPublishModelTests(ScriptingFixture scriptingFixture)
{
this.Fixture = scriptingFixture;
}
public ScriptingFixture Fixture { get; private set; }
public SqlTestDb Northwind { get { return this.Fixture.Database; } }
[Fact]
public async Task ListSchemaObjects()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingListObjectsParams requestParams = new ScriptingListObjectsParams
{
ConnectionString = this.Northwind.ConnectionString,
};
ScriptingListObjectsResult result = await testService.ListScriptingObjects(requestParams);
ScriptingListObjectsCompleteParams completeParameters = await testService.Driver.WaitForEvent(ScriptingListObjectsCompleteEvent.Type, TimeSpan.FromSeconds(30));
Assert.Equal<int>(ScriptingFixture.ObjectCountWithoutDatabase, completeParameters.DatabaseObjects.Count);
}
}
[Fact]
public async Task ScriptDatabaseSchema()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaOnly",
},
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
Assert.True(parameters.Success);
Assert.Equal<int>(ScriptingFixture.ObjectCountWithDatabase, planEvent.Count);
Assert.True(File.Exists(tempFile.FilePath));
Assert.True(new FileInfo(tempFile.FilePath).Length > 0);
}
}
[Fact]
public async Task ScriptDatabaseSchemaAndData()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaAndData",
},
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
ScriptingCompleteParams completeParameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
Assert.True(completeParameters.Success);
Assert.Equal<int>(ScriptingFixture.ObjectCountWithDatabase, planEvent.Count);
Assert.True(File.Exists(tempFile.FilePath));
Assert.True(new FileInfo(tempFile.FilePath).Length > 0);
}
}
[Fact]
public async Task ScriptTable()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaOnly",
},
ScriptingObjects = new List<ScriptingObject>
{
new ScriptingObject
{
Type = "Table",
Schema = "dbo",
Name = "Customers",
},
}
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
Assert.True(parameters.Success);
Assert.Equal<int>(2, planEvent.Count);
Assert.True(File.Exists(tempFile.FilePath));
Assert.True(new FileInfo(tempFile.FilePath).Length > 0);
}
}
[Fact]
public async Task ScriptTableUsingIncludeFilter()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaOnly",
},
IncludeObjectCriteria = new List<ScriptingObject>
{
new ScriptingObject
{
Type = "Table",
Schema = "dbo",
Name = "Customers",
},
}
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
Assert.True(parameters.Success);
Assert.Equal<int>(2, planEvent.Count);
Assert.True(File.Exists(tempFile.FilePath));
Assert.True(new FileInfo(tempFile.FilePath).Length > 0);
}
}
[Fact]
public async Task ScriptTableAndData()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaAndData",
},
ScriptingObjects = new List<ScriptingObject>
{
new ScriptingObject
{
Type = "Table",
Schema = "dbo",
Name = "Customers",
},
}
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingPlanNotificationParams planEvent = await testService.Driver.WaitForEvent(ScriptingPlanNotificationEvent.Type, TimeSpan.FromSeconds(30));
ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(30));
Assert.True(parameters.Success);
Assert.Equal<int>(2, planEvent.Count);
Assert.True(File.Exists(tempFile.FilePath));
Assert.True(new FileInfo(tempFile.FilePath).Length > 0);
}
}
[Fact]
public async Task ScriptTableDoesNotExist()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaOnly",
},
ScriptingObjects = new List<ScriptingObject>
{
new ScriptingObject
{
Type = "Table",
Schema = "dbo",
Name = "TableDoesNotExist",
},
}
};
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.Contains("The Table '[dbo].[TableDoesNotExist]' does not exist on the server.", parameters.ErrorDetails);
}
}
[Fact]
public async Task ScriptSchemaCancel()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = this.Northwind.ConnectionString,
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaAndData",
},
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingCancelResult cancelResult = await testService.CancelScript(result.OperationId);
ScriptingCompleteParams cancelEvent = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(10));
Assert.True(cancelEvent.Canceled);
}
}
[Fact]
public async Task ScriptSchemaInvalidConnectionString()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
using (SelfCleaningTempFile tempFile = new SelfCleaningTempFile())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = tempFile.FilePath,
ConnectionString = "I'm an invalid connection string",
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaAndData",
},
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(10));
Assert.True(parameters.HasError);
Assert.Equal("Error parsing ScriptingParams.ConnectionString property.", parameters.ErrorMessage);
}
}
[Fact]
public async Task ScriptSchemaInvalidFilePath()
{
using (TestServiceDriverProvider testService = new TestServiceDriverProvider())
{
ScriptingParams requestParams = new ScriptingParams
{
FilePath = "This path doesn't event exist",
ConnectionString = "Server=Temp;Database=Temp;User Id=Temp;Password=Temp",
ScriptOptions = new ScriptOptions
{
TypeOfDataToScript = "SchemaAndData",
},
};
ScriptingResult result = await testService.Script(requestParams);
ScriptingCompleteParams parameters = await testService.Driver.WaitForEvent(ScriptingCompleteEvent.Type, TimeSpan.FromSeconds(10));
Assert.True(parameters.HasError);
Assert.Equal("Invalid directory specified by the ScriptingParams.FilePath property.", parameters.ErrorMessage);
}
}
public void Dispose() { }
public class ScriptingFixture : IDisposable
{
public ScriptingFixture()
{
this.Database = SqlTestDb.CreateNew(TestServerType.OnPrem);
this.Database.RunQuery(Scripts.CreateNorthwindSchema, throwOnError: true);
Console.WriteLine("Northwind setup complete, database name: {0}", this.Database.DatabaseName);
}
/// <summary>
/// The count of object when scripting the entire database including the database object.
/// </summary>
public const int ObjectCountWithDatabase = 46;
/// <summary>
/// The count of objects when scripting the entire database excluding the database object.
/// </summary>
public const int ObjectCountWithoutDatabase = 45;
public SqlTestDb Database { get; private set; }
public void Dispose()
{
if (this.Database != null)
{
Console.WriteLine("Northwind cleanup, deleting database name: {0}", this.Database.DatabaseName);
this.Database.Dispose();
}
}
}
}
}

View File

@@ -19,6 +19,7 @@ using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.Hosting.Protocol.Channel;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts.ExecuteRequests;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Driver
{
@@ -124,6 +125,9 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Driver
this.QueueEventsForType(IntelliSenseReadyNotification.Type);
this.QueueEventsForType(QueryCompleteEvent.Type);
this.QueueEventsForType(PublishDiagnosticsNotification.Type);
this.QueueEventsForType(ScriptingCompleteEvent.Type);
this.QueueEventsForType(ScriptingPlanNotificationEvent.Type);
this.QueueEventsForType(ScriptingListObjectsCompleteEvent.Type);
}
/// <summary>

View File

@@ -83,6 +83,13 @@ namespace Microsoft.SqlTools.ServiceLayer.TestDriver.Driver
});
}
public async Task<TParams> WaitForEvent<TParams>(
EventType<TParams> eventType,
TimeSpan timeout)
{
return await WaitForEvent(eventType, (int) timeout.TotalMilliseconds);
}
public async Task<TParams> WaitForEvent<TParams>(
EventType<TParams> eventType,
int timeoutMilliseconds = 5000)

View File

@@ -0,0 +1,287 @@
//
// 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.Linq;
using Microsoft.SqlTools.ServiceLayer.Scripting;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Xunit;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Scripting
{
public class ScriptingMatcherTests
{
private static ScriptingObject Table_S1_Table1 = new ScriptingObject
{
Type = "Table",
Schema = "S1",
Name = "Table1",
};
private static ScriptingObject Table_S1_Table2 = new ScriptingObject
{
Type = "Table",
Schema = "S1",
Name = "Table2",
};
private static ScriptingObject Table_S2_Table1 = new ScriptingObject
{
Type = "Table",
Schema = "S2",
Name = "Table1",
};
private static ScriptingObject Table_S2_Table2 = new ScriptingObject
{
Type = "Table",
Schema = "S2",
Name = "Table2",
};
private static ScriptingObject View_S1_View1 = new ScriptingObject
{
Type = "View",
Schema = "S1",
Name = "View1",
};
private static ScriptingObject View_S1_View2 = new ScriptingObject
{
Type = "View",
Schema = "S1",
Name = "View2",
};
private static List<ScriptingObject> TestData = new List<ScriptingObject>
{
Table_S1_Table1,
Table_S1_Table2,
Table_S2_Table1,
Table_S2_Table2,
View_S1_View1,
View_S1_View2,
};
[Fact]
public void ScriptingMatchIncludeAll()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject[0],
excludeCriteria: new ScriptingObject[0],
candidates: TestData);
Assert.Equal<int>(6, results.Count());
}
[Fact]
public void ScriptingMatchIncludeNone()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject(),
excludeCriteria: new ScriptingObject(),
candidates: TestData);
Assert.Equal<int>(0, results.Count());
}
[Fact]
public void ScriptingMatchIncludeName()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Name = "Table1"},
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(2, results.Count());
}
[Fact]
public void ScriptingMatchIncludeNameWildcard()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Name = "*" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(6, results.Count());
}
public void ScriptingMatchIncludeNameWildcardPostfix()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Name = "Tab*" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(4, results.Count());
}
[Fact]
public void ScriptingMatchIncludeSchema()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Schema = "S2" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(2, results.Count());
}
[Fact]
public void ScriptingMatchIncludeSchemaWildcard()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Schema = "*" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(6, results.Count());
}
[Fact]
public void ScriptingMatchIncludeSchemaWildcardPostfix()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Schema = "S*" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(6, results.Count());
}
[Fact]
public void ScriptingMatchIncludeType()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Type="Table" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(4, results.Count());
}
[Fact]
public void ScriptingMatchIncludeNameAndSchema()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Schema = "S1", Name = "Table1" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(1, results.Count());
}
[Fact]
public void ScriptingMatchIncludeSchemaAndType()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: new ScriptingObject { Type="View", Schema = "S1" },
excludeCriteria: null,
candidates: TestData);
Assert.Equal<int>(2, results.Count());
}
[Fact]
public void ScriptingMatchExcludeName()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Name = "Table1" },
candidates: TestData);
Assert.Equal<int>(4, results.Count());
}
[Fact]
public void ScriptingMatchExcludeNameWildcard()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Name = "*" },
candidates: TestData);
Assert.Equal<int>(0, results.Count());
}
public void ScriptingMatchExcludeNameWildcardPostfix()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Name = "Tab*" },
candidates: TestData);
Assert.Equal<int>(4, results.Count());
}
[Fact]
public void ScriptingMatchExcludeSchema()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Schema = "S2" },
candidates: TestData);
Assert.Equal<int>(4, results.Count());
}
[Fact]
public void ScriptingMatchExcludeSchemaWildcard()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Schema = "*" },
candidates: TestData);
Assert.Equal<int>(0, results.Count());
}
[Fact]
public void ScriptingMatchExcludeSchemaWildcardPostfix()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Schema = "S*" },
candidates: TestData);
Assert.Equal<int>(0, results.Count());
}
[Fact]
public void ScriptingMatchExcludeType()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Type = "Table" },
candidates: TestData);
Assert.Equal<int>(2, results.Count());
}
[Fact]
public void ScriptingMatchExcludeNameAndSchema()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Schema = "S1", Name = "Table1" },
candidates: TestData);
Assert.Equal<int>(5, results.Count());
}
[Fact]
public void ScriptingMatchExcludeSchemaAndType()
{
IEnumerable<ScriptingObject> results = ScriptingObjectMatcher.Match(
includeCriteria: null,
excludeCriteria: new ScriptingObject { Type = "View", Schema = "S1" },
candidates: TestData);
Assert.Equal<int>(4, results.Count());
}
}
}

View File

@@ -19,7 +19,7 @@
"System.Runtime.Serialization.Primitives": "4.1.1",
"System.Data.Common": "4.1.0",
"System.Data.SqlClient": "4.4.0-sqltools-24613-04",
"Microsoft.SqlServer.Smo": "14.0.17028",
"Microsoft.SqlServer.Smo": "140.17049.0",
"System.Security.SecureString": "4.0.0",
"System.Collections.Specialized": "4.0.1",
"System.ComponentModel.TypeConverter": "4.1.0",