Insert sql bindings into Azure Functions (#1224)

* getting table name from a script

* add InsertSqlInputBindingOperation

* cleanup

* move azure functions stuff out of dacfx service

* cleanup

* add tests

* add another test

* cleanup

* add comments and connection string setting

* addressing comments

* change name to use add instead of insert
This commit is contained in:
Kim Santiago
2021-08-04 13:02:52 -07:00
committed by GitHub
parent a7703e63a4
commit b1653b25e4
15 changed files with 682 additions and 1 deletions

View File

@@ -0,0 +1,143 @@
//
// 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.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
using Microsoft.CodeAnalysis.CSharp;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
{
/// <summary>
/// Class to represent inserting a sql binding into an Azure Function
/// </summary>
class AddSqlBindingOperation
{
const string functionAttributeText = "FunctionName";
public AddSqlBindingParams Parameters { get; }
public AddSqlBindingOperation(AddSqlBindingParams parameters)
{
Validate.IsNotNull("parameters", parameters);
this.Parameters = parameters;
}
public ResultStatus AddBinding()
{
try
{
string text = File.ReadAllText(Parameters.filePath);
SyntaxTree tree = CSharpSyntaxTree.ParseText(text);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
// look for Azure Function to update
IEnumerable<MethodDeclarationSyntax> azureFunctionMethods = from methodDeclaration in root.DescendantNodes().OfType<MethodDeclarationSyntax>()
where methodDeclaration.AttributeLists.Count > 0
where methodDeclaration.AttributeLists.Where(a => a.Attributes.Where(attr => attr.Name.ToString().Contains(functionAttributeText) && attr.ArgumentList.Arguments.First().ToString().Equals($"\"{Parameters.functionName}\"")).Count() == 1).Count() == 1
select methodDeclaration;
if (azureFunctionMethods.Count() == 0)
{
return new ResultStatus()
{
Success = false,
ErrorMessage = SR.CouldntFindAzureFunction(Parameters.functionName, Parameters.filePath)
};
}
else if (azureFunctionMethods.Count() > 1)
{
return new ResultStatus()
{
Success = false,
ErrorMessage = SR.MoreThanOneAzureFunctionWithName(Parameters.functionName, Parameters.filePath)
};
}
MethodDeclarationSyntax azureFunction = azureFunctionMethods.First();
var newParam = this.Parameters.bindingType == BindingType.input ? this.GenerateInputBinding() : this.GenerateOutputBinding();
// Generate updated method with the new parameter
// normalizewhitespace gets rid of any newline whitespace in the leading trivia, so we add that back
var updatedMethod = azureFunction.AddParameterListParameters(newParam).NormalizeWhitespace().WithLeadingTrivia(azureFunction.GetLeadingTrivia()).WithTrailingTrivia(azureFunction.GetTrailingTrivia());
// Replace the node in the tree
root = root.ReplaceNode(azureFunction, updatedMethod);
// write updated tree to file
var workspace = new AdhocWorkspace();
var syntaxTree = CSharpSyntaxTree.ParseText(root.ToString());
var formattedNode = CodeAnalysis.Formatting.Formatter.Format(syntaxTree.GetRoot(), workspace);
StringBuilder sb = new StringBuilder(formattedNode.ToString());
string content = sb.ToString();
File.WriteAllText(Parameters.filePath, content);
return new ResultStatus()
{
Success = true
};
}
catch (Exception e)
{
return new ResultStatus()
{
Success = false,
ErrorMessage = e.ToString()
};
}
}
/// <summary>
/// Generates a parameter for the sql input binding that looks like
/// [Sql("select * from [dbo].[table1]", CommandType = System.Data.CommandType.Text, ConnectionStringSetting = "SqlConnectionString")] IEnumerable<Object> result
/// </summary>
private ParameterSyntax GenerateInputBinding()
{
// Create arguments for the Sql Input Binding attribute
var argumentList = SyntaxFactory.AttributeArgumentList();
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"select * from {Parameters.objectName}\"")));
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("CommandType"), SyntaxFactory.IdentifierName("System.Data.CommandType.Text"))));
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\""))));
// Create Sql Binding attribute
SyntaxList<AttributeListSyntax> attributesList = new SyntaxList<AttributeListSyntax>();
attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList))));
// Create new parameter
ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, new SyntaxTokenList(), SyntaxFactory.ParseTypeName("IEnumerable<Object>"), SyntaxFactory.Identifier("result"), null);
return newParam;
}
/// <summary>
/// Generates a parameter for the sql output binding that looks like
/// [Sql("[dbo].[table1]", ConnectionStringSetting = "SqlConnectionString")] out Object output
/// </summary>
private ParameterSyntax GenerateOutputBinding()
{
// Create arguments for the Sql Output Binding attribute
var argumentList = SyntaxFactory.AttributeArgumentList();
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.IdentifierName($"\"{Parameters.objectName}\"")));
argumentList = argumentList.AddArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.AssignmentExpression(SyntaxKind.SimpleAssignmentExpression, SyntaxFactory.IdentifierName("ConnectionStringSetting"), SyntaxFactory.IdentifierName($"\"{Parameters.connectionStringSetting}\""))));
SyntaxList<AttributeListSyntax> attributesList = new SyntaxList<AttributeListSyntax>();
attributesList = attributesList.Add(SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList<AttributeSyntax>(SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Sql")).WithArgumentList(argumentList))));
var syntaxTokenList = new SyntaxTokenList();
syntaxTokenList = syntaxTokenList.Add(SyntaxFactory.Token(SyntaxKind.OutKeyword));
ParameterSyntax newParam = SyntaxFactory.Parameter(attributesList, syntaxTokenList, SyntaxFactory.ParseTypeName(typeof(Object).Name), SyntaxFactory.Identifier("output"), null);
return newParam;
}
}
}

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.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions
{
/// <summary>
/// Main class for Azure Functions service
/// </summary>
class AzureFunctionsService
{
private static readonly Lazy<AzureFunctionsService> instance = new Lazy<AzureFunctionsService>(() => new AzureFunctionsService());
/// <summary>
/// Gets the singleton instance object
/// </summary>
public static AzureFunctionsService Instance
{
get { return instance.Value; }
}
/// <summary>
/// Initializes the service instance
/// </summary>
/// <param name="serviceHost"></param>
public void InitializeService(ServiceHost serviceHost)
{
serviceHost.SetRequestHandler(AddSqlBindingRequest.Type, this.HandleAddSqlBindingRequest);
}
/// <summary>
/// Handles request to add sql binding into Azure Functions
/// </summary>
public async Task HandleAddSqlBindingRequest(AddSqlBindingParams parameters, RequestContext<ResultStatus> requestContext)
{
try
{
AddSqlBindingOperation operation = new AddSqlBindingOperation(parameters);
ResultStatus result = operation.AddBinding();
await requestContext.SendResult(result);
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
}
}

View File

@@ -0,0 +1,60 @@
//
// 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;
using Microsoft.SqlTools.ServiceLayer.SchemaCompare.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
namespace Microsoft.SqlTools.ServiceLayer.AzureFunctions.Contracts
{
/// <summary>
/// Binding types for sql bindings for Azure Functions
/// </summary>
public enum BindingType
{
input,
output
}
/// <summary>
/// Parameters for adding a sql binding
/// </summary>
public class AddSqlBindingParams
{
/// <summary>
/// Gets or sets the filePath
/// </summary>
public string filePath { get; set; }
/// <summary>
/// Gets or sets the binding type
/// </summary>
public BindingType bindingType { get; set; }
/// <summary>
/// Gets or sets the function name
/// </summary>
public string functionName { get; set; }
/// <summary>
/// Gets or sets the object name
/// </summary>
public string objectName { get; set; }
/// <summary>
/// Gets or sets the connection string setting
/// </summary>
public string connectionStringSetting { get; set; }
}
/// <summary>
/// Defines the Add Sql Binding request
/// </summary>
class AddSqlBindingRequest
{
public static readonly RequestType<AddSqlBindingParams, ResultStatus> Type =
RequestType<AddSqlBindingParams, ResultStatus>.Create("azureFunctions/sqlBinding");
}
}

View File

@@ -10,6 +10,7 @@ using Microsoft.SqlTools.Hosting;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Admin;
using Microsoft.SqlTools.ServiceLayer.Agent;
using Microsoft.SqlTools.ServiceLayer.AzureFunctions;
using Microsoft.SqlTools.ServiceLayer.Cms;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.DacFx;
@@ -133,6 +134,9 @@ namespace Microsoft.SqlTools.ServiceLayer
SchemaCompare.SchemaCompareService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(SchemaCompareService.Instance);
AzureFunctions.AzureFunctionsService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(AzureFunctionsService.Instance);
ServerConfigService.Instance.InitializeService(serviceHost);
serviceProvider.RegisterSingleService(ServerConfigService.Instance);

View File

@@ -3278,6 +3278,16 @@ namespace Microsoft.SqlTools.ServiceLayer
return Keys.GetString(Keys.SqlAssessmentUnsuppoertedEdition, editionCode);
}
public static string CouldntFindAzureFunction(string functionName, string fileName)
{
return Keys.GetString(Keys.CouldntFindAzureFunction, functionName, fileName);
}
public static string MoreThanOneAzureFunctionWithName(string functionName, string fileName)
{
return Keys.GetString(Keys.MoreThanOneAzureFunctionWithName, functionName, fileName);
}
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Keys
{
@@ -4555,6 +4565,12 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string SqlAssessmentUnsuppoertedEdition = "SqlAssessmentUnsuppoertedEdition";
public const string CouldntFindAzureFunction = "CouldntFindAzureFunction";
public const string MoreThanOneAzureFunctionWithName = "MoreThanOneAzureFunctionWithName";
private Keys()
{ }

View File

@@ -1854,4 +1854,14 @@
<comment>.
Parameters: 0 - editionCode (int) </comment>
</data>
<data name="CouldntFindAzureFunction" xml:space="preserve">
<value>Couldn&apos;t find Azure function with FunctionName {0} in {1}</value>
<comment>.
Parameters: 0 - functionName (string), 1 - fileName (string) </comment>
</data>
<data name="MoreThanOneAzureFunctionWithName" xml:space="preserve">
<value>More than one Azure function found with the FunctionName {0} in {1}</value>
<comment>.
Parameters: 0 - functionName (string), 1 - fileName (string) </comment>
</data>
</root>

View File

@@ -848,4 +848,9 @@ SchemaCompareSessionNotFound = Could not find the schema compare session to canc
SqlAssessmentGenerateScriptTaskName = Generate SQL Assessment script
SqlAssessmentQueryInvalidOwnerUri = Not connected to a server
SqlAssessmentConnectingError = Cannot connect to the server
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
SqlAssessmentUnsuppoertedEdition(int editionCode) = Unsupported engine edition {0}
############################################################################
# Azure Functions
CouldntFindAzureFunction(string functionName, string fileName) = Couldn't find Azure function with FunctionName '{0}' in {1}
MoreThanOneAzureFunctionWithName(string functionName, string fileName) = More than one Azure function found with the FunctionName '{0}' in {1}

View File

@@ -2161,6 +2161,16 @@
<target state="new">No External Streaming Job creation TSQL found (EXEC sp_create_streaming_job statement).</target>
<note></note>
</trans-unit>
<trans-unit id="CouldntFindAzureFunction">
<source>Couldn't find Azure function with FunctionName {0} in {1}</source>
<target state="new">Couldn't find Azure function with FunctionName {0} in {1}</target>
<note></note>
</trans-unit>
<trans-unit id="MoreThanOneAzureFunctionWithName">
<source>More than one Azure function found with the FunctionName {0} in {1}</source>
<target state="new">More than one Azure function found with the FunctionName {0} in {1}</target>
<note></note>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -21,6 +21,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.SqlServer.DACFx" />
<PackageReference Include="Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider" />
<PackageReference Include="System.Text.Encoding.CodePages" />