Adding star expression expansion (#1270)

This commit is contained in:
Aasim Khan
2021-10-27 16:59:05 -07:00
committed by GitHub
parent 26d4339277
commit e246bc5325
6 changed files with 309 additions and 30 deletions

View File

@@ -25,6 +25,7 @@
<PackageReference Update="Microsoft.Azure.Kusto.Language" Version="9.0.4"/>
<PackageReference Update="Microsoft.SqlServer.Assessment" Version="[1.0.305]" />
<PackageReference Update="Microsoft.SqlServer.Migration.Assessment" Version="1.0.20210902.7" />
<PackageReference Update="Microsoft.SqlServer.Management.SqlParser" Version="160.21267.55" />
<PackageReference Update="Microsoft.Azure.OperationalInsights" Version="1.0.0" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" />

View File

@@ -9,11 +9,15 @@ using System.Linq;
using System.Threading;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.Metadata;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Management;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
@@ -728,5 +732,131 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return help;
}
/// <summary>
/// Give suggestions for sql star expansion.
/// </summary>
/// <param name="scriptDocumentInfo">Document info containing the current cursor position</param>
/// <returns>Completion item array containing the expanded star suggestion</returns>
public static CompletionItem[] ExpandSqlStarExpression(ScriptDocumentInfo scriptDocumentInfo)
{
//Fetching the star expression node in sql script.
SqlSelectStarExpression selectStarExpression = AutoCompleteHelper.TryGetSelectStarStatement(scriptDocumentInfo.ScriptParseInfo.ParseResult.Script, scriptDocumentInfo);
if (selectStarExpression == null)
{
return null;
}
// Getting SQL object identifier for star expressions like a.*
SqlObjectIdentifier starObjectIdentifier = null;
if (selectStarExpression.Children.Any())
{
starObjectIdentifier = (SqlObjectIdentifier)selectStarExpression.Children.ElementAt(0);
}
List<ITabular> boundedTableList = selectStarExpression.BoundTables.ToList();
IList<string> columnNames = new List<string>();
/*
We include table names in 2 conditions.
1. When there are multiple tables to avoid column ambiguity
2. When there is single table with an alias
*/
bool includeTableName = boundedTableList.Count > 1 || (boundedTableList.Count == 1 && boundedTableList[0] != boundedTableList[0].Unaliased);
// Handing case for object identifiers where the column names will contain the identifier for eg: a.* becomes a.column_name
if (starObjectIdentifier != null)
{
string objectIdentifierName = starObjectIdentifier.ObjectName.ToString();
ITabular relatedTable = boundedTableList.Single(t => t.Name == objectIdentifierName);
columnNames = relatedTable.Columns.Select(c => String.Format("{0}.{1}", Utils.MakeSqlBracket(objectIdentifierName), Utils.MakeSqlBracket(c.Name))).ToList();
}
else
{
foreach (var table in boundedTableList)
{
foreach (var column in table.Columns)
{
if (includeTableName)
{
columnNames.Add($"{Utils.MakeSqlBracket(table.Name)}.{Utils.MakeSqlBracket(column.Name)}"); // Including table names in case of multiple tables to avoid column ambiguity errors.
}
else
{
columnNames.Add(Utils.MakeSqlBracket(column.Name));
}
}
}
}
if (columnNames == null || columnNames.Count == 0)
{
return null;
}
var insertText = String.Join(String.Format(",{0}", Environment.NewLine), columnNames.ToArray()); // Adding a new line after every column name
var completionItems = new CompletionItem[] {
new CompletionItem
{
InsertText = insertText,
Label = insertText,
Detail = insertText,
Kind = CompletionItemKind.Text,
/*
Vscode/ADS only shows completion items that match the text present in the editor. However, in case of star expansion that is never going to happen as columns names are different than '*'.
Therefore adding an explicit filterText that contains the original star expression to trick vscode/ADS into showing this suggestion item.
*/
FilterText = selectStarExpression.Sql,
Preselect = true,
TextEdit = new TextEdit {
NewText = insertText,
Range = new Range {
Start = new Position{
Line = scriptDocumentInfo.StartLine,
Character = selectStarExpression.StartLocation.ColumnNumber - 1
},
End = new Position {
Line = scriptDocumentInfo.StartLine,
Character = selectStarExpression.EndLocation.ColumnNumber - 1
}
}
}
}
};
return completionItems;
}
public static SqlSelectStarExpression TryGetSelectStarStatement(SqlCodeObject currentNode, ScriptDocumentInfo scriptDocumentInfo)
{
if(currentNode == null || scriptDocumentInfo == null)
{
return null;
}
// Checking if the current node is a sql select star expression.
if (currentNode is SqlSelectStarExpression)
{
return currentNode as SqlSelectStarExpression;
}
// Visiting children to get the the sql select star expression.
foreach (SqlCodeObject child in currentNode.Children)
{
// Visiting only those children where the cursor is present.
int childStartLineNumber = child.StartLocation.LineNumber - 1;
int childEndLineNumber = child.EndLocation.LineNumber - 1;
SqlSelectStarExpression childStarExpression = TryGetSelectStarStatement(child, scriptDocumentInfo);
if ((childStartLineNumber < scriptDocumentInfo.StartLine ||
childStartLineNumber == scriptDocumentInfo.StartLine && child.StartLocation.ColumnNumber <= scriptDocumentInfo.StartColumn) &&
(childEndLineNumber > scriptDocumentInfo.StartLine ||
childEndLineNumber == scriptDocumentInfo.StartLine && child.EndLocation.ColumnNumber >= scriptDocumentInfo.EndColumn) &&
childStarExpression != null)
{
return childStarExpression;
}
}
return null;
}
}
}

View File

@@ -43,7 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// Main class for Language Service functionality including anything that requires knowledge of
/// the language to perform, such as definitions, intellisense, etc.
/// </summary>
public class LanguageService: IDisposable
public class LanguageService : IDisposable
{
#region Singleton Instance Implementation
@@ -193,7 +193,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
if (workspaceServiceInstance == null)
{
workspaceServiceInstance = WorkspaceService<SqlToolsSettings>.Instance;
workspaceServiceInstance = WorkspaceService<SqlToolsSettings>.Instance;
}
return workspaceServiceInstance;
}
@@ -407,7 +407,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
if (result != null && result.Errors.Count() == 0)
{
syntaxResult.Parseable = true;
} else
}
else
{
syntaxResult.Parseable = false;
string[] errorMessages = new string[result.Errors.Count()];
@@ -558,7 +559,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
};
}
// turn off this code until needed (10/28/2016)
// turn off this code until needed (10/28/2016)
#if false
private async Task HandleReferencesRequest(
ReferencesParams referencesParams,
@@ -796,19 +797,19 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
// Send a notification to signal that autocomplete is ready
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() {OwnerUri = connInfo.OwnerUri});
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = connInfo.OwnerUri });
});
}
else
{
// Send a notification to signal that autocomplete is ready
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() {OwnerUri = rebuildParams.OwnerUri});
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri });
}
}
catch (Exception ex)
{
Logger.Write(TraceEventType.Error, "Unknown error " + ex.ToString());
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() {OwnerUri = rebuildParams.OwnerUri});
await ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = rebuildParams.OwnerUri });
}
}
@@ -873,14 +874,16 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Validate.IsNotNull(nameof(changeParams), changeParams);
Validate.IsNotNull(nameof(changeParams), changeParams.Uri);
bool shouldBlock = false;
if (SQL_LANG.Equals(changeParams.Language, StringComparison.OrdinalIgnoreCase)) {
if (SQL_LANG.Equals(changeParams.Language, StringComparison.OrdinalIgnoreCase))
{
shouldBlock = !ServiceHost.ProviderName.Equals(changeParams.Flavor, StringComparison.OrdinalIgnoreCase);
}
if (SQL_CMD_LANG.Equals(changeParams.Language, StringComparison.OrdinalIgnoreCase))
{
shouldBlock = true; // the provider will continue to be mssql
}
if (shouldBlock) {
if (shouldBlock)
{
this.nonMssqlUriMap.AddOrUpdate(changeParams.Uri, true, (k, oldValue) => true);
if (CurrentWorkspace.ContainsFile(changeParams.Uri))
{
@@ -893,8 +896,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
this.nonMssqlUriMap.TryRemove(changeParams.Uri, out value);
// should rebuild intellisense when re-considering as sql
RebuildIntelliSenseParams param = new RebuildIntelliSenseParams { OwnerUri = changeParams.Uri };
await HandleRebuildIntelliSenseNotification(param, eventContext);
}
await HandleRebuildIntelliSenseNotification(param, eventContext);
}
}
catch (Exception ex)
{
@@ -939,27 +942,27 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
if (connInfo == null || !parseInfo.IsConnected)
{
// parse on separate thread so stack size can be increased
var parseThread = new Thread(() =>
// parse on separate thread so stack size can be increased
var parseThread = new Thread(() =>
{
try
{
try
{
// parse current SQL file contents to retrieve a list of errors
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo.ParseResult,
this.DefaultParseOptions);
scriptFile.Contents,
parseInfo.ParseResult,
this.DefaultParseOptions);
parseInfo.ParseResult = parseResult;
}
catch (Exception e)
{
parseInfo.ParseResult = parseResult;
}
catch (Exception e)
{
// Log the exception but don't rethrow it to prevent parsing errors from crashing SQL Tools Service
Logger.Write(TraceEventType.Error, string.Format("An unexpected error occured while parsing: {0}", e.ToString()));
}
}, ConnectedBindingQueue.QueueThreadStackSize);
parseThread.Start();
parseThread.Join();
}
}, ConnectedBindingQueue.QueueThreadStackSize);
parseThread.Start();
parseThread.Join();
}
else
{
@@ -1003,7 +1006,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
return null;
});
queueItem.ItemProcessed.WaitOne();
queueItem.ItemProcessed.WaitOne();
}
}
catch (Exception ex)
@@ -1019,7 +1022,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
else
{
Logger.Write(TraceEventType.Warning, "Binding metadata lock timeout in ParseAndBind");
Logger.Write(TraceEventType.Warning, "Binding metadata lock timeout in ParseAndBind");
}
return parseInfo.ParseResult;
@@ -1062,7 +1065,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
PrepopulateCommonMetadata(info, scriptInfo, this.BindingQueue);
// Send a notification to signal that autocomplete is ready
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() {OwnerUri = info.OwnerUri});
ServiceHostInstance.SendEvent(IntelliSenseReadyNotification.Type, new IntelliSenseReadyParams() { OwnerUri = info.OwnerUri });
});
}
@@ -1249,7 +1252,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
finally
{
Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
Monitor.Exit(scriptParseInfo.BuildingMetadataLock);
}
}
}
@@ -1656,6 +1659,13 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
this.currentCompletionParseInfo = scriptParseInfo;
resultCompletionItems = result.CompletionItems;
// Expanding star expressions in query
CompletionItem[] starExpansionSuggestion = AutoCompleteHelper.ExpandSqlStarExpression(scriptDocumentInfo);
if (starExpansionSuggestion != null)
{
return starExpansionSuggestion;
}
// if there are no completions then provide the default list
if (resultCompletionItems == null)
{

View File

@@ -29,6 +29,7 @@
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="Microsoft.SqlServer.Assessment" />
<PackageReference Include="Microsoft.SqlServer.Migration.Assessment" />
<PackageReference Include="Microsoft.SqlServer.Management.SqlParser"/>
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom.NRT">
<Aliases>ASAScriptDom</Aliases>

View File

@@ -401,5 +401,64 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.LanguageServer
Assert.True(notif.Diagnostics.Length == errors, $"Notification errors {notif.Diagnostics.Length} are not as expected {errors}");
return true;
}
[Test]
//simple select star with single column in the table
[TestCase("select * from wildcard_test_table", 0, 8, "CREATE TABLE wildcard_test_table(col1 int)", "[col1]")]
//simple select star with multiple columns in the table
[TestCase("select * from wildcard_test_table", 0, 8, "CREATE TABLE wildcard_test_table(col1 int, col2 int, \"col3\" int)", "[col1],\r\n[col2],\r\n[col3]")]
//select star query with special characters in the table
[TestCase("select * from wildcard_test_table", 0, 8, "CREATE TABLE wildcard_test_table(\"col[$$$#]\" int)", "[col[$$$#]]]")]
//select star query for multiple tables
[TestCase("select * from wildcard_test_table1 CROSS JOIN wildcard_test_table2", 0, 8, "CREATE TABLE wildcard_test_table1(table1col1 int); CREATE TABLE wildcard_test_table2(table2col1 int)", "[wildcard_test_table1].[table1col1],\r\n[wildcard_test_table2].[table2col1]")]
//select star query with object identifier in associated with * eg: a.*
[TestCase("select *, a.* from wildcard_test_table1 as a CROSS JOIN wildcard_test_table2", 0, 13, "CREATE TABLE wildcard_test_table1(table1col1 int); CREATE TABLE wildcard_test_table2(table2col1 int)", "[a].[table1col1]")]
//select star query with nested from statement
[TestCase("select * from (select col2 from wildcard_test_table1) as alias", 0, 8, "CREATE TABLE wildcard_test_table1(col1 int, col2 int)", "[col2]")]
public async Task ExpandSqlStarExpressionsTest(string sqlStarQuery, int cursorLine, int cursorColumn, string createTableQueries, string expectedStarExpansionInsertText)
{
var testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, null, null, "WildCardExpansionTest");
try
{
var connectionInfoResult = LiveConnectionHelper.InitLiveConnectionInfo(testDb.DatabaseName);
var langService = LanguageService.Instance;
await langService.UpdateLanguageServiceOnConnection(connectionInfoResult.ConnectionInfo);
connectionInfoResult.ScriptFile.SetFileContents(sqlStarQuery);
var textDocumentPosition =
connectionInfoResult.TextDocumentPosition ??
new TextDocumentPosition()
{
TextDocument = new TextDocumentIdentifier
{
Uri = connectionInfoResult.ScriptFile.ClientUri
},
Position = new Position
{
Line = cursorLine,
Character = cursorColumn //Position of the star expression
}
};
// Now create tables that should show up in the completion list
testDb.RunQuery(createTableQueries);
// And refresh the cache
await langService.HandleRebuildIntelliSenseNotification(
new RebuildIntelliSenseParams() { OwnerUri = connectionInfoResult.ScriptFile.ClientUri },
new TestEventContext());
// Now we should expect to see the star expansion show up in the completion list
var starExpansionCompletionItem = await langService.GetCompletionItems(
textDocumentPosition, connectionInfoResult.ScriptFile, connectionInfoResult.ConnectionInfo);
Assert.AreEqual(expectedStarExpansionInsertText, starExpansionCompletionItem[0].InsertText, "Star expansion not found");
}
finally
{
testDb.Cleanup();
}
}
}
}

View File

@@ -5,8 +5,10 @@
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Completion;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Moq;
@@ -186,5 +188,81 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.LanguageServer
// verify that send result was called with a completion array
requestContext.Verify(m => m.SendResult(It.IsAny<CompletionItem[]>()), Times.Once());
}
public ScriptDocumentInfo CreateSqlStarTestFile(string sqlText, int startLine, int startColumn)
{
var uri = "file://nofile.sql";
var textDocumentPosition = new TextDocumentPosition()
{
TextDocument = new TextDocumentIdentifier()
{
Uri = uri
},
Position = new Position()
{
Line = startLine,
Character = startColumn
}
};
var scriptFile = new ScriptFile()
{
ClientUri = uri,
Contents = sqlText
};
ParseResult parseResult = langService.ParseAndBind(scriptFile, null);
ScriptParseInfo scriptParseInfo = langService.GetScriptParseInfo(scriptFile.ClientUri, true);
return new ScriptDocumentInfo(textDocumentPosition, scriptFile, scriptParseInfo);
}
[Test]
//complete select query with the cursor at * should return a sqlselectstarexpression object.
[TestCase("select * from sys.all_objects", 0, 8, "SelectStarExpression is not returned on complete select query with star")]
//incomplete select query with the cursor at * should sqlselectstarexpression
[TestCase("select * ", 0, 8, "SelectStarExpression is returned on an incomplete select query with star")]
//method should return sqlselectstarexpression on *s with object identifiers.
[TestCase("select a.* from sys.all_objects as a", 0, 10, "SelectStarExpression returned on star expression with object identifier")]
public void TryGetSqlSelectStarStatementNotNullTests(string sqlQuery, int cursorLine, int cursorColumn, string errorValidationMessage)
{
InitializeTestObjects();
var testFile = CreateSqlStarTestFile(sqlQuery, cursorLine, cursorColumn);
Assert.NotNull(AutoCompleteHelper.TryGetSelectStarStatement(testFile.ScriptParseInfo.ParseResult.Script, testFile), errorValidationMessage);
}
[Test]
//complete select query with the cursor not at * should return null.
[TestCase("select * from sys.all_objects", 0, 0, "null is not returned when the cursor is not at a star expression")]
//file with no text should return null
[TestCase("", 0, 0, "null is not returned on file with empty sql text")]
//file with out of bounds cursor position should return null
[TestCase("select * from sys.all_objects", 0, 100, "null is not returned when the cursor is out of bounds.")]
public void TryGetSqlSelectStarStatementNullTests(string sqlQuery, int cursorLine, int cursorColumn, string errorValidationMessage)
{
InitializeTestObjects();
var testFile = CreateSqlStarTestFile(sqlQuery, cursorLine, cursorColumn);
Assert.Null(AutoCompleteHelper.TryGetSelectStarStatement(testFile.ScriptParseInfo.ParseResult.Script, testFile), errorValidationMessage);
}
[Test]
public void TryGetSqlSelectStarStatementNullFileTest()
{
Assert.Null(AutoCompleteHelper.TryGetSelectStarStatement(null, null), "null is not returned on null file");
}
[Test]
[TestCase("select a.*, * from sys.all_objects as a CROSS JOIN sys.databases", 0, 10, "a.*")]
[TestCase("select a.*, * from sys.all_objects as a CROSS JOIN sys.databases", 0, 13, "*")]
public void TryGetSqlSelectStarStatmentMulitpleStarExpressionsTest(string sqlQuery, int cursorLine, int cursorColumn, string expectedStarExpressionSqlText)
{
InitializeTestObjects();
var testFile = CreateSqlStarTestFile(sqlQuery, cursorLine, cursorColumn);
var starExpressionTest = AutoCompleteHelper.TryGetSelectStarStatement(testFile.ScriptParseInfo.ParseResult.Script, testFile).Sql;
Assert.AreEqual(expectedStarExpressionSqlText, expectedStarExpressionSqlText, string.Format("correct SelectStarExpression is not returned."));
}
}
}