Initial metadata and scripting services (#280)

* Initial metadata service and scripting service files

* Simple metadata lookup with SMO objects

* Add metadata type class

* Remove SMO from metadata service.

* Cleanup metadata service SQL

* Initial MetadataService test

* Add scripting commands

* Add metadata test case

* Remove sleep used for testing

* Use random table name in metadata test

* Add scripting tests
This commit is contained in:
Karl Burtram
2017-03-14 22:35:17 -07:00
committed by GitHub
parent 9b1e07907e
commit 7ba2011a1e
9 changed files with 588 additions and 1 deletions

6
.gitignore vendored
View File

@@ -284,7 +284,7 @@ Session.vim
# docfx generated files
_site
metadata
docs/metadata
# Stuff from cake
/artifacts/
@@ -292,3 +292,7 @@ metadata
/.dotnet/
/test/Microsoft.SqlTools.ServiceLayer.TestEnvConfig/Properties/launchSettings.json
/test/Microsoft.SqlTools.ServiceLayer.PerfTests/Properties/launchSettings.json
# Test output files
*cycle*txt*
*input*txt*

View File

@@ -10,7 +10,9 @@ using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.EditData;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.Metadata;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.Scripting;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace;
@@ -76,6 +78,12 @@ namespace Microsoft.SqlTools.ServiceLayer
EditDataService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(EditDataService.Instance);
MetadataService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(MetadataService.Instance);
ScriptingService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(ScriptingService.Instance);
InitializeHostedServices(serviceProvider, serviceHost);
serviceHost.InitializeRequestHandlers();

View File

@@ -0,0 +1,26 @@
//
// 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.Metadata.Contracts
{
public class MetadataQueryParams
{
public string OwnerUri { get; set; }
}
public class MetadataQueryResult
{
public ObjectMetadata[] Metadata { get; set; }
}
public class MetadataListRequest
{
public static readonly
RequestType<MetadataQueryParams, MetadataQueryResult> Type =
RequestType<MetadataQueryParams, MetadataQueryResult>.Create("metadata/list");
}
}

View File

@@ -0,0 +1,32 @@
//
// 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.Metadata.Contracts
{
/// <summary>
/// Metadata type enumeration
/// </summary>
public enum MetadataType
{
Table = 0,
View = 1,
SProc = 2,
Function = 3
}
/// <summary>
/// Object metadata information
/// </summary>
public class ObjectMetadata
{
public MetadataType MetadataType { get; set; }
public string MetadataTypeName { get; set; }
public string Schema { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,167 @@
//
// 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.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Metadata
{
/// <summary>
/// Main class for Metadata Service functionality
/// </summary>
public sealed class MetadataService
{
private static readonly Lazy<MetadataService> LazyInstance = new Lazy<MetadataService>(() => new MetadataService());
public static MetadataService Instance => LazyInstance.Value;
private static ConnectionService connectionService = null;
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal static ConnectionService ConnectionServiceInstance
{
get
{
if (connectionService == null)
{
connectionService = ConnectionService.Instance;
}
return connectionService;
}
set
{
connectionService = value;
}
}
/// <summary>
/// Initializes the Metadata Service instance
/// </summary>
/// <param name="serviceHost"></param>
/// <param name="context"></param>
public void InitializeService(ServiceHost serviceHost)
{
serviceHost.SetRequestHandler(MetadataListRequest.Type, HandleMetadataListRequest);
}
/// <summary>
/// Handle a metadata query request
/// </summary>
internal static async Task HandleMetadataListRequest(
MetadataQueryParams metadataParams,
RequestContext<MetadataQueryResult> requestContext)
{
try
{
ConnectionInfo connInfo;
MetadataService.ConnectionServiceInstance.TryFindConnection(
metadataParams.OwnerUri,
out connInfo);
var metadata = new List<ObjectMetadata>();
if (connInfo != null)
{
SqlConnection sqlConn = OpenMetadataConnection(connInfo);
ReadMetadata(sqlConn, metadata);
}
await requestContext.SendResult(new MetadataQueryResult()
{
Metadata = metadata.ToArray()
});
}
catch (Exception ex)
{
await requestContext.SendError(ex.ToString());
}
}
/// <summary>
/// Create a SqlConnection to use for querying metadata
/// </summary>
internal static SqlConnection OpenMetadataConnection(ConnectionInfo connInfo)
{
try
{
// increase the connection timeout to at least 30 seconds and and build connection string
// enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections
int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout;
bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo;
connInfo.ConnectionDetails.ConnectTimeout = Math.Max(30, originalTimeout ?? 0);
connInfo.ConnectionDetails.PersistSecurityInfo = true;
string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails);
connInfo.ConnectionDetails.ConnectTimeout = originalTimeout;
connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo;
// open a dedicated binding server connection
SqlConnection sqlConn = new SqlConnection(connectionString);
sqlConn.Open();
return sqlConn;
}
catch (Exception)
{
}
return null;
}
/// <summary>
/// Read metadata for the current connection
/// </summary>
internal static void ReadMetadata(SqlConnection sqlConn, List<ObjectMetadata> metadata)
{
string sql =
@"SELECT s.name AS schema_name, o.[name] AS object_name, o.[type] AS object_type
FROM sys.all_objects o
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
WHERE o.is_ms_shipped != 1
AND (o.[type] = 'P' OR o.[type] = 'V' OR o.[type] = 'U')
ORDER BY object_type, schema_name, object_name";
using (SqlCommand sqlCommand = new SqlCommand(sql, sqlConn))
{
using (var reader = sqlCommand.ExecuteReader())
{
while (reader.Read())
{
var schemaName = reader[0] as string;
var objectName = reader[1] as string;
var objectType = reader[2] as string;
MetadataType metadataType;
if (objectType.StartsWith("V"))
{
metadataType = MetadataType.View;
}
else if (objectType.StartsWith("P"))
{
metadataType = MetadataType.SProc;
}
else
{
metadataType = MetadataType.Table;
}
metadata.Add(new ObjectMetadata
{
MetadataType = metadataType,
Schema = schemaName,
Name = objectName
});
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
//
// 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;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Scripting.Contracts
{
/// <summary>
/// The type of scripting operation requested
/// </summary>
public enum ScriptOperation
{
Select = 0,
Create = 1,
Insert = 2,
Update = 3,
Delete = 4
}
/// <summary>
/// Script as request parameter type
/// </summary>
public class ScriptingScriptAsParams
{
public string OwnerUri { get; set; }
public ScriptOperation Operation { get; set; }
public ObjectMetadata Metadata { get; set; }
}
/// <summary>
/// Script as request result type
/// </summary>
public class ScriptingScriptAsResult
{
public string OwnerUri { get; set; }
public string Script { get; set; }
}
/// <summary>
/// Script as request message type
/// </summary>
public class ScriptingScriptAsRequest
{
public static readonly
RequestType<ScriptingScriptAsParams, ScriptingScriptAsResult> Type =
RequestType<ScriptingScriptAsParams, ScriptingScriptAsResult>.Create("scripting/scriptas");
}
}

View File

@@ -0,0 +1,82 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Main class for Scripting Service functionality
/// </summary>
public sealed class ScriptingService
{
private static readonly Lazy<ScriptingService> LazyInstance = new Lazy<ScriptingService>(() => new ScriptingService());
public static ScriptingService Instance => LazyInstance.Value;
/// <summary>
/// Initializes the Scripting Service instance
/// </summary>
/// <param name="serviceHost"></param>
/// <param name="context"></param>
public void InitializeService(ServiceHost serviceHost)
{
serviceHost.SetRequestHandler(ScriptingScriptAsRequest.Type, HandleScriptingScriptAsRequest);
}
/// <summary>
/// Handles script as request messages
/// </summary>
/// <param name="scriptingParams"></param>
/// <param name="requestContext"></param>
internal static async Task HandleScriptingScriptAsRequest(
ScriptingScriptAsParams scriptingParams,
RequestContext<ScriptingScriptAsResult> requestContext)
{
string script = string.Empty;
if (scriptingParams.Operation == ScriptOperation.Select)
{
script = string.Format(
@"SELECT *
FROM {0}.{1}",
scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name);
}
else if (scriptingParams.Operation == ScriptOperation.Create)
{
script = string.Format(
@"CREATE {0}.{1}",
scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name);
}
else if (scriptingParams.Operation == ScriptOperation.Update)
{
script = string.Format(
@"UPDATE {0}.{1}",
scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name);
}
else if (scriptingParams.Operation == ScriptOperation.Insert)
{
script = string.Format(
@"INSERT {0}.{1}",
scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name);
}
else if (scriptingParams.Operation == ScriptOperation.Delete)
{
script = string.Format(
@"DELETE {0}.{1}",
scriptingParams.Metadata.Schema, scriptingParams.Metadata.Name);
}
await requestContext.SendResult(new ScriptingScriptAsResult()
{
OwnerUri = scriptingParams.OwnerUri,
Script = script
});
}
}
}

View File

@@ -0,0 +1,96 @@
//
// 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.ServiceLayer.Workspace.Contracts;
using Xunit;
using Microsoft.SqlTools.ServiceLayer.Test.Common;
using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
using Microsoft.SqlTools.ServiceLayer.Metadata;
using System.Collections.Generic;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
using System.Data.SqlClient;
using System;
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata
{
/// <summary>
/// Tests for the Metadata service component
/// </summary>
public class MetadataServiceTests
{
private string testTableSchema = "dbo";
private string testTableName = "MetadataTestTable";
private LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects()
{
var textDocument = new TextDocumentPosition
{
TextDocument = new TextDocumentIdentifier { Uri = Constants.OwnerUri },
Position = new Position
{
Line = 0,
Character = 0
}
};
var result = LiveConnectionHelper.InitLiveConnectionInfo();
result.TextDocumentPosition = textDocument;
return result;
}
private void CreateTestTable(SqlConnection sqlConn)
{
string sql = string.Format("IF OBJECT_ID('{0}.{1}', 'U') IS NULL CREATE TABLE {0}.{1}(id int)",
this.testTableSchema, this.testTableName);
using (var sqlCommand = new SqlCommand(sql, sqlConn))
{
sqlCommand.ExecuteNonQuery();
}
}
private void DeleteTestTable(SqlConnection sqlConn)
{
string sql = string.Format("IF OBJECT_ID('{0}.{1}', 'U') IS NOT NULL DROP TABLE {0}.{1}",
this.testTableSchema, this.testTableName);
using (var sqlCommand = new SqlCommand(sql, sqlConn))
{
sqlCommand.ExecuteNonQuery();
}
}
/// <summary>
/// Verify that the metadata service correctly returns details for user tables
/// </summary>
[Fact]
public void MetadataReturnsUserTable()
{
this.testTableName += new Random().Next(1000000, 9999999).ToString();
var result = GetLiveAutoCompleteTestObjects();
var sqlConn = MetadataService.OpenMetadataConnection(result.ConnectionInfo);
Assert.NotNull(sqlConn);
CreateTestTable(sqlConn);
var metadata = new List<ObjectMetadata>();
MetadataService.ReadMetadata(sqlConn, metadata);
Assert.NotNull(metadata.Count > 0);
bool foundTestTable = false;
foreach (var item in metadata)
{
if (string.Equals(item.Schema, this.testTableSchema, StringComparison.OrdinalIgnoreCase)
&& string.Equals(item.Name, this.testTableName, StringComparison.OrdinalIgnoreCase))
{
foundTestTable = true;
break;
}
}
Assert.True(foundTestTable);
DeleteTestTable(sqlConn);
}
}
}

View File

@@ -0,0 +1,118 @@
//
// 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.ServiceLayer.Workspace.Contracts;
using Xunit;
using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
using Microsoft.SqlTools.ServiceLayer.Scripting;
using Moq;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Scripting
{
/// <summary>
/// Tests for the scripting service component
/// </summary>
public class ScriptingServiceTests
{
private const string SchemaName = "sys";
private const string TableName = "all_objects";
private LiveConnectionHelper.TestConnectionResult GetLiveAutoCompleteTestObjects()
{
var textDocument = new TextDocumentPosition
{
TextDocument = new TextDocumentIdentifier { Uri = Test.Common.Constants.OwnerUri },
Position = new Position
{
Line = 0,
Character = 0
}
};
var result = LiveConnectionHelper.InitLiveConnectionInfo();
result.TextDocumentPosition = textDocument;
return result;
}
private async Task<Mock<RequestContext<ScriptingScriptAsResult>>> SendAndValidateScriptRequest(ScriptOperation operation)
{
var result = GetLiveAutoCompleteTestObjects();
var requestContext = new Mock<RequestContext<ScriptingScriptAsResult>>();
requestContext.Setup(x => x.SendResult(It.IsAny<ScriptingScriptAsResult>())).Returns(Task.FromResult(new object()));
var scriptingParams = new ScriptingScriptAsParams
{
OwnerUri = Test.Common.Constants.OwnerUri,
Operation = operation,
Metadata = new ObjectMetadata()
{
MetadataType = MetadataType.Table,
MetadataTypeName = "View",
Schema = SchemaName,
Name = TableName
}
};
await ScriptingService.HandleScriptingScriptAsRequest(scriptingParams, requestContext.Object);
requestContext.Verify(x => x.SendResult(It.Is<ScriptingScriptAsResult>(
i => i.Script.Contains(operation.ToString().ToUpper())
&& i.Script.Contains(TableName)
&& i.Script.Contains(SchemaName))));
return requestContext;
}
/// <summary>
/// Verify the script as select request
/// </summary>
[Fact]
public async void ScriptingScriptAsSelect()
{
await SendAndValidateScriptRequest(ScriptOperation.Select);
}
/// <summary>
/// Verify the script as create request
/// </summary>
[Fact]
public async void ScriptingScriptAsCreate()
{
await SendAndValidateScriptRequest(ScriptOperation.Create);
}
/// <summary>
/// Verify the script as insert request
/// </summary>
[Fact]
public async void ScriptingScriptAsInsert()
{
await SendAndValidateScriptRequest(ScriptOperation.Insert);
}
/// <summary>
/// Verify the script as update request
/// </summary>
[Fact]
public async void ScriptingScriptAsUpdate()
{
await SendAndValidateScriptRequest(ScriptOperation.Update);
}
/// <summary>
/// Verify the script as delete request
/// </summary>
[Fact]
public async void ScriptingScriptAsDelete()
{
await SendAndValidateScriptRequest(ScriptOperation.Delete);
}
}
}