// // 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; using System.Data.Common; using System.Data.SqlClient; using System.Reflection; using System.Threading.Tasks; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.SmoMetadataProvider; using Microsoft.SqlServer.Management.SqlParser; using Microsoft.SqlServer.Management.SqlParser.Binder; using Microsoft.SqlServer.Management.SqlParser.Intellisense; using Microsoft.SqlServer.Management.SqlParser.MetadataProvider; using Microsoft.SqlServer.Management.SqlParser.Parser; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.Test.Utility; using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Microsoft.SqlTools.Test.Utility; using Moq; using Moq.Protected; using Xunit; namespace Microsoft.SqlTools.ServiceLayer.Test.LanguageServices { /// /// Tests for the ServiceHost Language Service tests /// public class LanguageServiceTests { #region "Diagnostics tests" [Fact] public void TestSmo() { SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); connectionBuilder["Data Source"] = "sqltools11"; connectionBuilder["Integrated Security"] = false; connectionBuilder["User Id"] = "sa"; connectionBuilder["Password"] = "Yukon900"; connectionBuilder["Initial Catalog"] = "master"; string connectionString = connectionBuilder.ToString(); var conn = new SqlConnection(connectionString); var sqlConn = new ServerConnection(conn); var server = new Server(sqlConn); string s = ""; foreach (Database db2 in server.Databases) { s += db2.Name; } var metadata = SmoMetadataProvider.CreateConnectedProvider(sqlConn); var db = metadata.Server.Databases["master"]; } [Fact] public void TestSmoMetadataProvider() { SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder(); //connectionBuilder["Data Source"] = "sqltools11"; connectionBuilder["Data Source"] = "localhost"; connectionBuilder["Integrated Security"] = false; connectionBuilder["User Id"] = "sa"; connectionBuilder["Password"] = "Yukon900"; connectionBuilder["Initial Catalog"] = "master"; try { var sqlConnection = new SqlConnection(connectionBuilder.ToString()); var connection = new ServerConnection(sqlConnection); var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(connection); var binder = BinderProvider.CreateBinder(metadataProvider); var displayInfoProvider = new MetadataDisplayInfoProvider(); //string sql = @"SELECT * FROM sys.objects;"; string sql = @"SELECT "; ParseOptions parseOptions = new ParseOptions(); ParseResult parseResult = Parser.IncrementalParse( sql, null, parseOptions); List parseResults = new List(); parseResults.Add(parseResult); binder.Bind(parseResults, "master", BindMode.Batch); var comp = Resolver.FindCompletions(parseResult, 1, 8, displayInfoProvider); comp.Add(null); } finally { // Check if we failed to create a binder object. If so, we temporarely // use a no-op binder which has the effect of turning off binding. We // also set a timer that after the specified timeout expires removes // the no-op timer (object becomes dead) which would give clients of // this class an opportunity to remove it and create a new one. } } // [Fact] // public void TestAltParse() // { // var ls = new LanguageService(); // ls.AltParse(); // } /// /// Verify that the latest SqlParser (2016 as of this writing) is used by default /// [Fact] public void LatestSqlParserIsUsedByDefault() { // This should only parse correctly on SQL server 2016 or newer const string sql2016Text = @"CREATE SECURITY POLICY [FederatedSecurityPolicy]" + "\r\n" + @"ADD FILTER PREDICATE [rls].[fn_securitypredicate]([CustomerId])" + "\r\n" + @"ON [dbo].[Customer];"; LanguageService service = TestObjects.GetTestLanguageService(); // parse var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sql2016Text); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); // verify that no errors are detected Assert.Equal(0, fileMarkers.Length); } /// /// Verify that the SQL parser correctly detects errors in text /// [Fact] public void ParseSelectStatementWithoutErrors() { // sql statement with no errors const string sqlWithErrors = "SELECT * FROM sys.objects"; // get the test service LanguageService service = TestObjects.GetTestLanguageService(); // parse the sql statement var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sqlWithErrors); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); // verify there are no errors Assert.Equal(0, fileMarkers.Length); } /// /// Verify that the SQL parser correctly detects errors in text /// [Fact] public void ParseSelectStatementWithError() { // sql statement with errors const string sqlWithErrors = "SELECT *** FROM sys.objects"; // get test service LanguageService service = TestObjects.GetTestLanguageService(); // parse sql statement var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sqlWithErrors); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); // verify there is one error Assert.Equal(1, fileMarkers.Length); // verify the position of the error Assert.Equal(9, fileMarkers[0].ScriptRegion.StartColumnNumber); Assert.Equal(1, fileMarkers[0].ScriptRegion.StartLineNumber); Assert.Equal(10, fileMarkers[0].ScriptRegion.EndColumnNumber); Assert.Equal(1, fileMarkers[0].ScriptRegion.EndLineNumber); } /// /// Verify that the SQL parser correctly detects errors in text /// [Fact] public void ParseMultilineSqlWithErrors() { // multiline sql with errors const string sqlWithErrors = "SELECT *** FROM sys.objects;\n" + "GO\n" + "SELECT *** FROM sys.objects;\n"; // get test service LanguageService service = TestObjects.GetTestLanguageService(); // parse sql var scriptFile = new ScriptFile(); scriptFile.SetFileContents(sqlWithErrors); ScriptFileMarker[] fileMarkers = service.GetSemanticMarkers(scriptFile); // verify there are two errors Assert.Equal(2, fileMarkers.Length); // check position of first error Assert.Equal(9, fileMarkers[0].ScriptRegion.StartColumnNumber); Assert.Equal(1, fileMarkers[0].ScriptRegion.StartLineNumber); Assert.Equal(10, fileMarkers[0].ScriptRegion.EndColumnNumber); Assert.Equal(1, fileMarkers[0].ScriptRegion.EndLineNumber); // check position of second error Assert.Equal(9, fileMarkers[1].ScriptRegion.StartColumnNumber); Assert.Equal(3, fileMarkers[1].ScriptRegion.StartLineNumber); Assert.Equal(10, fileMarkers[1].ScriptRegion.EndColumnNumber); Assert.Equal(3, fileMarkers[1].ScriptRegion.EndLineNumber); } #endregion #region "Autocomplete Tests" /// /// Creates a mock db command that returns a predefined result set /// public static DbCommand CreateTestCommand(Dictionary[][] data) { var commandMock = new Mock { CallBase = true }; var commandMockSetup = commandMock.Protected() .Setup("ExecuteDbDataReader", It.IsAny()); commandMockSetup.Returns(new TestDbDataReader(data)); return commandMock.Object; } /// /// Creates a mock db connection that returns predefined data when queried for a result set /// public DbConnection CreateMockDbConnection(Dictionary[][] data) { var connectionMock = new Mock { CallBase = true }; connectionMock.Protected() .Setup("CreateDbCommand") .Returns(CreateTestCommand(data)); return connectionMock.Object; } /// /// Verify that the autocomplete service returns tables for the current connection as suggestions /// [Fact] public void TablesAreReturnedAsAutocompleteSuggestions() { // Result set for the query of database tables Dictionary[] data = { new Dictionary { {"name", "master" } }, new Dictionary { {"name", "model" } } }; var mockFactory = new Mock(); mockFactory.Setup(factory => factory.CreateSqlConnection(It.IsAny())) .Returns(CreateMockDbConnection(new[] {data})); var connectionService = new ConnectionService(mockFactory.Object); var autocompleteService = new AutoCompleteService(); autocompleteService.ConnectionServiceInstance = connectionService; autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); // Open a connection // The cache should get updated as part of this ConnectParams connectionRequest = TestObjects.GetTestConnectionParams(); var connectionResult = connectionService.Connect(connectionRequest); Assert.NotEmpty(connectionResult.ConnectionId); // Check that there is one cache created in the auto complete service Assert.Equal(1, autocompleteService.GetCacheCount()); // Check that we get table suggestions for an autocomplete request TextDocumentPosition position = new TextDocumentPosition(); position.Uri = connectionRequest.OwnerUri; position.Position = new Position(); position.Position.Line = 1; position.Position.Character = 1; var items = autocompleteService.GetCompletionItems(position); Assert.Equal(2, items.Length); Assert.Equal("master", items[0].Label); Assert.Equal("model", items[1].Label); } /// /// Verify that only one intellisense cache is created for two documents using /// the autocomplete service when they share a common connection. /// [Fact] public void OnlyOneCacheIsCreatedForTwoDocumentsWithSameConnection() { var connectionService = new ConnectionService(TestObjects.GetTestSqlConnectionFactory()); var autocompleteService = new AutoCompleteService(); autocompleteService.ConnectionServiceInstance = connectionService; autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); // Open two connections ConnectParams connectionRequest1 = TestObjects.GetTestConnectionParams(); connectionRequest1.OwnerUri = "file:///my/first/file.sql"; ConnectParams connectionRequest2 = TestObjects.GetTestConnectionParams(); connectionRequest2.OwnerUri = "file:///my/second/file.sql"; var connectionResult1 = connectionService.Connect(connectionRequest1); Assert.NotEmpty(connectionResult1.ConnectionId); var connectionResult2 = connectionService.Connect(connectionRequest2); Assert.NotEmpty(connectionResult2.ConnectionId); // Verify that only one intellisense cache is created to service both URI's Assert.Equal(1, autocompleteService.GetCacheCount()); } /// /// Verify that two different intellisense caches and corresponding autocomplete /// suggestions are provided for two documents with different connections. /// [Fact] public void TwoCachesAreCreatedForTwoDocumentsWithDifferentConnections() { const string testDb1 = "my_db"; const string testDb2 = "my_other_db"; // Result set for the query of database tables Dictionary[] data1 = { new Dictionary { {"name", "master" } }, new Dictionary { {"name", "model" } } }; Dictionary[] data2 = { new Dictionary { {"name", "master" } }, new Dictionary { {"name", "my_table" } }, new Dictionary { {"name", "my_other_table" } } }; var mockFactory = new Mock(); mockFactory.Setup(factory => factory.CreateSqlConnection(It.Is(x => x.Contains(testDb1)))) .Returns(CreateMockDbConnection(new[] {data1})); mockFactory.Setup(factory => factory.CreateSqlConnection(It.Is(x => x.Contains(testDb2)))) .Returns(CreateMockDbConnection(new[] {data2})); var connectionService = new ConnectionService(mockFactory.Object); var autocompleteService = new AutoCompleteService(); autocompleteService.ConnectionServiceInstance = connectionService; autocompleteService.InitializeService(Microsoft.SqlTools.ServiceLayer.Hosting.ServiceHost.Instance); // Open connections // The cache should get updated as part of this ConnectParams connectionRequest = TestObjects.GetTestConnectionParams(); connectionRequest.OwnerUri = "file:///my/first/sql/file.sql"; connectionRequest.Connection.DatabaseName = testDb1; var connectionResult = connectionService.Connect(connectionRequest); Assert.NotEmpty(connectionResult.ConnectionId); // Check that there is one cache created in the auto complete service Assert.Equal(1, autocompleteService.GetCacheCount()); // Open second connection ConnectParams connectionRequest2 = TestObjects.GetTestConnectionParams(); connectionRequest2.OwnerUri = "file:///my/second/sql/file.sql"; connectionRequest2.Connection.DatabaseName = testDb2; var connectionResult2 = connectionService.Connect(connectionRequest2); Assert.NotEmpty(connectionResult2.ConnectionId); // Check that there are now two caches in the auto complete service Assert.Equal(2, autocompleteService.GetCacheCount()); // Check that we get 2 different table suggestions for autocomplete requests TextDocumentPosition position = new TextDocumentPosition(); position.Uri = connectionRequest.OwnerUri; position.Position = new Position(); position.Position.Line = 1; position.Position.Character = 1; var items = autocompleteService.GetCompletionItems(position); Assert.Equal(2, items.Length); Assert.Equal("master", items[0].Label); Assert.Equal("model", items[1].Label); TextDocumentPosition position2 = new TextDocumentPosition(); position2.Uri = connectionRequest2.OwnerUri; position2.Position = new Position(); position2.Position.Line = 1; position2.Position.Character = 1; var items2 = autocompleteService.GetCompletionItems(position2); Assert.Equal(3, items2.Length); Assert.Equal("master", items2[0].Label); Assert.Equal("my_table", items2[1].Label); Assert.Equal("my_other_table", items2[2].Label); } #endregion } }