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

@@ -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>