Add Always Encrypted Parameterization Functionality (#953)

This commit is contained in:
Jeff Trimmer
2020-05-05 12:01:24 -07:00
committed by GitHub
parent e3f1789f18
commit 82eed06847
16 changed files with 1618 additions and 9 deletions

View File

@@ -0,0 +1,38 @@
//
// 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 Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Helpers;
namespace Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Exceptions
{
/// <summary>
/// ParameterizationFormatException is used to surface format exceptions encountered in the TSQL batch to perform
/// auto-parameterization of literals for Always Encrypted.
/// </summary>
public class ParameterizationFormatException : FormatException
{
public readonly int LineNumber;
public readonly string LiteralValue;
public readonly string CodeSenseMessage;
public readonly string LogMessage;
public readonly string SqlDatatype;
public readonly string CSharpDataType;
public ParameterizationFormatException(MessageHelper.MessageType type, string variableName, string sqlDataType, string cSharpDataType, string literalValue, int lineNumber)
: this(type, variableName, sqlDataType, cSharpDataType, literalValue, lineNumber, exception: null) { }
public ParameterizationFormatException(MessageHelper.MessageType type, string variableName, string sqlDataType, string cSharpDataType, string literalValue, int lineNumber, Exception exception)
: base(MessageHelper.GetLocalizedMessage(type, variableName, sqlDataType, literalValue), exception)
{
LineNumber = lineNumber;
LiteralValue = literalValue;
SqlDatatype = sqlDataType;
CSharpDataType = cSharpDataType;
CodeSenseMessage = Message;
LogMessage = MessageHelper.GetLocaleInvariantMessage(type, variableName, sqlDataType) + ", Literal Value: " + literalValue + ", On line: " + lineNumber + ", CSharp DataType: " + cSharpDataType;
}
}
}

View File

@@ -0,0 +1,24 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
namespace Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Exceptions
{
/// <summary>
/// ParameterizationParsingException is used to surface parse errors encountered in the TSQL batch while creating a parse tree
/// </summary>
public class ParameterizationParsingException : Exception
{
public readonly int ColumnNumber;
public readonly int LineNumber;
public ParameterizationParsingException(int lineNumber, int columnNumber, string errorMessage) : base(errorMessage)
{
LineNumber = lineNumber;
ColumnNumber = columnNumber;
}
}
}

View File

@@ -0,0 +1,18 @@
//
// 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.AutoParameterizaition.Exceptions
{
internal class ParameterizationScriptTooLargeException : ParameterizationParsingException
{
public readonly int ScriptLength;
// LineNumber and ColumnNumber are defaulted to 1 because this exception is thrown if the script is very large and lineNumber and columnNumber dont make much sense
public ParameterizationScriptTooLargeException(int scriptLength, string errorMessage) : base(lineNumber: 1, columnNumber: 1, errorMessage: errorMessage)
{
ScriptLength = scriptLength;
}
}
}

View File

@@ -0,0 +1,53 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Globalization;
namespace Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Helpers
{
public class MessageHelper
{
private static readonly string ERROR_MESSAGE_TEMPLATE = "Unable to Convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(System.Data.SqlDbType).";
private static readonly string DATE_TIME_ERROR_MESSAGE_TEMPLATE = "Unable to Convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(System.Data.SqlDbType), as it used an unsupported date/time format. Use one of the supported Date/time formats.";
private static readonly string BINARY_LITERAL_PREFIX_MISSING_ERROR_TEMPLATE = "Unable to Convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(System.Data.SqlDbType), as prefix 0x is expected for a binary literals.";
internal static string GetLocalizedMessage(MessageType type, string variableName, string sqlDataType, string literalValue)
{
switch (type)
{
case MessageType.ERROR_MESSAGE:
return SR.ErrorMessage(variableName, sqlDataType, literalValue);
case MessageType.DATE_TIME_ERROR_MESSAGE:
return SR.DateTimeErrorMessage(variableName, sqlDataType, literalValue);
case MessageType.BINARY_LITERAL_PREFIX_MISSING_ERROR:
return SR.BinaryLiteralPrefixMissingError(variableName, sqlDataType, literalValue);
default:
return "";
}
}
internal static string GetLocaleInvariantMessage(MessageType type, string variableName, string sqlDataType)
{
switch (type)
{
case MessageType.ERROR_MESSAGE:
return string.Format(CultureInfo.InvariantCulture, ERROR_MESSAGE_TEMPLATE, variableName, sqlDataType);
case MessageType.DATE_TIME_ERROR_MESSAGE:
return string.Format(CultureInfo.InvariantCulture, DATE_TIME_ERROR_MESSAGE_TEMPLATE, variableName, sqlDataType);
case MessageType.BINARY_LITERAL_PREFIX_MISSING_ERROR:
return string.Format(CultureInfo.InvariantCulture, BINARY_LITERAL_PREFIX_MISSING_ERROR_TEMPLATE, variableName, sqlDataType);
default:
return "";
}
}
public enum MessageType
{
ERROR_MESSAGE,
DATE_TIME_ERROR_MESSAGE,
BINARY_LITERAL_PREFIX_MISSING_ERROR,
}
}
}

View File

@@ -0,0 +1,718 @@
//
// 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.SqlTypes;
using System.Globalization;
using System.Linq;
using Microsoft.Data.SqlClient;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Exceptions;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Helpers;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.AutoParameterizaition
{
internal class ScalarExpressionTransformer : TSqlFragmentVisitor
{
#region datetimeFormats
private static readonly string[] SUPPORTED_ISO_DATE_TIME_FORMATS = {
"yyyyMMdd HH:mm:ss.fffffff",
"yyyyMMdd HH:mm:ss.ffffff",
"yyyyMMdd HH:mm:ss.fffff",
"yyyyMMdd HH:mm:ss.ffff",
"yyyyMMdd HH:mm:ss.fff",
"yyyyMMdd HH:mm:ss.ff",
"yyyyMMdd HH:mm:ss.f",
"yyyyMMdd HH:mm:ss",
"yyyyMMdd HH:mm",
"yyyyMMdd",
"yyyy-MM-ddTHH:mm:ss.fffffff",
"yyyy-MM-ddTHH:mm:ss.ffffff",
"yyyy-MM-ddTHH:mm:ss.fffff",
"yyyy-MM-ddTHH:mm:ss.ffff",
"yyyy-MM-ddTHH:mm:ss.fff",
"yyyy-MM-ddTHH:mm:ss.ff",
"yyyy-MM-ddTHH:mm:ss.f",
"yyyy-MM-ddTHH:mm:ss",
"yyyy-MM-ddTHH:mm",
"yyyy-MM-dd",
};
private static readonly string[] SUPPORTED_ISO_DATE_FORMATS = {
"yyyyMMdd",
"yyyy-MM-dd",
};
private static readonly string[] SUPPORTED_ISO_DATE_TIME_OFFSET_FORMATS = {
// 121025 12:32:10.1234567 +01:00 zzz in the below format represents +01:00
"yyyyMMdd HH:mm:ss.fffffff zzz",
"yyyyMMdd HH:mm:ss.ffffff zzz",
"yyyyMMdd HH:mm:ss.fffff zzz",
"yyyyMMdd HH:mm:ss.ffff zzz",
"yyyyMMdd HH:mm:ss.fff zzz",
"yyyyMMdd HH:mm:ss.ff zzz",
"yyyyMMdd HH:mm:ss.f zzz",
"yyyyMMdd HH:mm:ss zzz",
"yyyyMMdd HH:mm zzz",
"yyyyMMdd zzz",
"yyyy-MM-ddTHH:mm:ss.fffffff zzz",
"yyyy-MM-ddTHH:mm:ss.ffffff zzz",
"yyyy-MM-ddTHH:mm:ss.fffff zzz",
"yyyy-MM-ddTHH:mm:ss.ffff zzz",
"yyyy-MM-ddTHH:mm:ss.fff zzz",
"yyyy-MM-ddTHH:mm:ss.ff zzz",
"yyyy-MM-ddTHH:mm:ss.f zzz",
"yyyy-MM-ddTHH:mm:ss zzz",
"yyyy-MM-ddTHH:mm zzz",
"yyyy-MM-dd zzz",
//19991212 19:30:30.1234567Z K in the below format represents Z
"yyyyMMdd HH:mm:ss.fffffffK",
"yyyyMMdd HH:mm:ss.ffffffK",
"yyyyMMdd HH:mm:ss.fffffK",
"yyyyMMdd HH:mm:ss.ffffK",
"yyyyMMdd HH:mm:ss.fffK",
"yyyyMMdd HH:mm:ss.ffK",
"yyyyMMdd HH:mm:ss.fK",
"yyyyMMdd HH:mm:ssK",
"yyyyMMdd HH:mmK",
"yyyyMMddK",
"yyyy-MM-ddTHH:mm:ss.fffffffK",
"yyyy-MM-ddTHH:mm:ss.ffffffK",
"yyyy-MM-ddTHH:mm:ss.fffffK",
"yyyy-MM-ddTHH:mm:ss.ffffK",
"yyyy-MM-ddTHH:mm:ss.fffK",
"yyyy-MM-ddTHH:mm:ss.ffK",
"yyyy-MM-ddTHH:mm:ss.fK",
"yyyy-MM-ddTHH:mm:ssK",
"yyyy-MM-ddTHH:mmK",
"yyyy-MM-ddK",
};
#endregion
private const string C_SHARP_BYTE_ARRAY = "byte[]";
private readonly bool IsCodeSenseRequest;
private bool IsNegative = false;
private ScalarExpression CurrentScalarExpression;
public SqlDataTypeOption SqlDataTypeOption;
public IList<Literal> SqlDataTypeParameters;
public string VariableName;
public IList<SqlParameter> Parameters { get; private set; }
private readonly IList<ScriptFileMarker> CodeSenseErrors;
public ScalarExpressionTransformer(bool isCodeSenseRequest, IList<ScriptFileMarker> codeSenseErrors)
{
Parameters = new List<SqlParameter>();
IsCodeSenseRequest = isCodeSenseRequest;
CodeSenseErrors = codeSenseErrors;
}
public override void ExplicitVisit(ScalarExpression node)
{
if (node == null)
{
return;
}
CurrentScalarExpression = node;
if (node is Literal literal)
{
if (ShouldParameterize(literal))
{
var variableReference = new VariableReference();
string parameterName = GetParameterName();
variableReference.Name = parameterName;
AddToParameterCollection(literal, parameterName, SqlDataTypeOption, SqlDataTypeParameters);
CurrentScalarExpression = variableReference;
}
return;
}
if (node is UnaryExpression unaryExpression)
{
ScalarExpression expression = unaryExpression.Expression;
if (expression != null)
{
if (unaryExpression.UnaryExpressionType.Equals(UnaryExpressionType.Negative))
{
IsNegative = !IsNegative;
}
ExplicitVisit(expression);
}
base.ExplicitVisit(node); //let the base class finish up
return;
}
if (node is ParenthesisExpression parenthesisExpression)
{
ScalarExpression expression = parenthesisExpression.Expression;
if (expression != null)
{
ScalarExpression tempScalarExpression = CurrentScalarExpression;
ExplicitVisit(expression);
parenthesisExpression.Expression = GetTransformedExpression();
CurrentScalarExpression = tempScalarExpression;
}
base.ExplicitVisit(node); // let the base class finish up
}
}
public ScalarExpression GetTransformedExpression()
{
return CurrentScalarExpression;
}
public void Reset()
{
SqlDataTypeOption = SqlDataTypeOption.VarChar;
SqlDataTypeParameters = null;
VariableName = null;
IsNegative = false;
Parameters.Clear();
}
/// <summary>
/// Converts a hex string to a byte[]
/// Note: this method expects "0x" prefix to be stripped off from the input string
/// For example, to convert the string "0xFFFF" to byte[] the input to this method should be "FFFF"
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
private byte[] StringToByteArray(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
private void AddToParameterCollection(Literal literal, string parameterName, SqlDataTypeOption sqlDataTypeOption, IList<Literal> sqlDataTypeParameters)
{
SqlParameter sqlParameter = new SqlParameter();
string literalValue = literal.Value;
object parsedValue = null;
SqlDbType paramType = SqlDbType.VarChar;
bool parseSuccessful = true;
switch (sqlDataTypeOption)
{
case SqlDataTypeOption.Binary:
paramType = SqlDbType.Binary;
try
{
parsedValue = TryParseBinaryLiteral(literalValue, VariableName, SqlDbType.Binary, literal.StartLine);
}
catch (ParameterizationFormatException)
{
if (IsCodeSenseRequest)
{
parseSuccessful = false;
AddCodeSenseErrorItem(MessageHelper.MessageType.BINARY_LITERAL_PREFIX_MISSING_ERROR, literal, literal.Value, VariableName, SqlDbType.Binary.ToString());
}
else
{
throw;
}
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Binary.ToString(), C_SHARP_BYTE_ARRAY, literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.VarBinary:
paramType = SqlDbType.VarBinary;
try
{
parsedValue = TryParseBinaryLiteral(literalValue, VariableName, SqlDbType.VarBinary, literal.StartLine);
ExtractSize(sqlDataTypeParameters, sqlParameter);
}
catch (ParameterizationFormatException)
{
if (IsCodeSenseRequest)
{
parseSuccessful = false;
string sqlDataTypeString = GetSqlDataTypeStringOneParameter(SqlDbType.VarBinary, sqlDataTypeParameters);
AddCodeSenseErrorItem(MessageHelper.MessageType.BINARY_LITERAL_PREFIX_MISSING_ERROR, literal, literalValue, VariableName, sqlDataTypeString);
}
else
{
throw;
}
}
catch (Exception e)
{
parseSuccessful = false;
string sqlDataTypeString = GetSqlDataTypeStringOneParameter(SqlDbType.VarBinary, sqlDataTypeParameters);
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, sqlDataTypeString, C_SHARP_BYTE_ARRAY, literalValue, literal.StartLine, e);
}
break;
//Integer literals of form 24.0 will not be supported
case SqlDataTypeOption.BigInt:
paramType = SqlDbType.BigInt;
long parsedLong;
literalValue = IsNegative ? "-" + literalValue : literalValue;
if (long.TryParse(literalValue, out parsedLong))
{
parsedValue = parsedLong;
}
else
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.BigInt.ToString(), "Int64", literalValue, literal.StartLine, null);
}
break;
case SqlDataTypeOption.Int:
paramType = SqlDbType.Int;
int parsedInt;
literalValue = IsNegative ? "-" + literalValue : literalValue;
if (int.TryParse(literalValue, out parsedInt))
{
parsedValue = parsedInt;
}
else
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Int.ToString(), "Int32", literalValue, literal.StartLine, null);
}
break;
case SqlDataTypeOption.SmallInt:
paramType = SqlDbType.SmallInt;
short parsedShort;
literalValue = IsNegative ? "-" + literalValue : literalValue;
if (short.TryParse(literalValue, out parsedShort))
{
parsedValue = parsedShort;
}
else
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.SmallInt.ToString(), "Int16", literalValue, literal.StartLine, null);
}
break;
case SqlDataTypeOption.TinyInt:
paramType = SqlDbType.TinyInt;
byte parsedByte;
literalValue = IsNegative ? "-" + literalValue : literalValue;
if (byte.TryParse(literalValue, out parsedByte))
{
parsedValue = parsedByte;
}
else
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.TinyInt.ToString(), "Byte", literalValue, literal.StartLine, null);
}
break;
case SqlDataTypeOption.Real:
paramType = SqlDbType.Real;
literalValue = IsNegative ? "-" + literalValue : literalValue;
try
{
parsedValue = SqlSingle.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Real.ToString(), "SqlSingle", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.Float:
paramType = SqlDbType.Float;
literalValue = IsNegative ? "-" + literalValue : literalValue;
try
{
parsedValue = SqlDouble.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Float.ToString(), "SqlDouble", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.Decimal:
case SqlDataTypeOption.Numeric:
paramType = SqlDbType.Decimal;
ExtractPrecisionAndScale(sqlDataTypeParameters, sqlParameter);
literalValue = IsNegative ? "-" + literalValue : literalValue;
try
{
parsedValue = SqlDecimal.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
string sqlDecimalDataType = sqlDataTypeParameters != null ? (SqlDbType.Decimal + "(" + sqlDataTypeParameters[0] + ", " + sqlDataTypeParameters[1] + ")") : "";
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, sqlDecimalDataType, "SqlDecimal", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.Money:
paramType = SqlDbType.Money;
literalValue = IsNegative ? "-" + literalValue : literalValue;
try
{
parsedValue = SqlMoney.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Money.ToString(), "SqlMoney", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.SmallMoney:
paramType = SqlDbType.SmallMoney;
literalValue = IsNegative ? "-" + literalValue : literalValue;
try
{
parsedValue = SqlMoney.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.SmallMoney.ToString(), "SqlMoney", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.DateTime:
paramType = SqlDbType.DateTime;
try
{
parsedValue = ParseDateTime(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.DATE_TIME_ERROR_MESSAGE, literal, VariableName, SqlDbType.DateTime.ToString(), "DateTime", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.SmallDateTime:
paramType = SqlDbType.SmallDateTime;
try
{
parsedValue = ParseDateTime(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.DATE_TIME_ERROR_MESSAGE, literal, VariableName, SqlDbType.SmallDateTime.ToString(), "DateTime", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.DateTime2:
paramType = SqlDbType.DateTime2;
try
{
parsedValue = ParseDateTime(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.DATE_TIME_ERROR_MESSAGE, literal, VariableName, SqlDbType.DateTime2.ToString(), "DateTime", literalValue, literal.StartLine, e);
}
ExtractPrecision(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.Date:
paramType = SqlDbType.Date;
try
{
parsedValue = ParseDate(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.DATE_TIME_ERROR_MESSAGE, literal, VariableName, SqlDbType.Date.ToString(), "DateTime", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.DateTimeOffset:
paramType = SqlDbType.DateTimeOffset;
try
{
parsedValue = ParseDateTimeOffset(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.DATE_TIME_ERROR_MESSAGE, literal, VariableName, SqlDbType.DateTimeOffset.ToString(), "DateTimeOffset", literalValue, literal.StartLine, e);
}
ExtractPrecision(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.Time:
paramType = SqlDbType.Time;
try
{
parsedValue = TimeSpan.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Time.ToString(), "TimeSpan", literalValue, literal.StartLine, e);
}
ExtractPrecision(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.Char:
paramType = SqlDbType.Char;
ExtractSize(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.VarChar:
paramType = SqlDbType.VarChar;
ExtractSize(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.NChar:
paramType = SqlDbType.NChar;
ExtractSize(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.NVarChar:
paramType = SqlDbType.NVarChar;
ExtractSize(sqlDataTypeParameters, sqlParameter);
break;
case SqlDataTypeOption.UniqueIdentifier:
paramType = SqlDbType.UniqueIdentifier;
try
{
parsedValue = SqlGuid.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.UniqueIdentifier.ToString(), "SqlGuid", literalValue, literal.StartLine, e);
}
break;
case SqlDataTypeOption.Bit:
paramType = SqlDbType.Bit;
try
{
parsedValue = Byte.Parse(literalValue);
}
catch (Exception e)
{
parseSuccessful = false;
HandleError(MessageHelper.MessageType.ERROR_MESSAGE, literal, VariableName, SqlDbType.Bit.ToString(), "Byte", literalValue, literal.StartLine, e);
}
break;
default:
break;
}
if (parseSuccessful)
{
sqlParameter.ParameterName = parameterName;
sqlParameter.SqlDbType = paramType;
sqlParameter.Value = parsedValue ?? literalValue;
sqlParameter.Direction = ParameterDirection.Input;
Parameters.Add(sqlParameter);
}
}
private string GetSqlDataTypeStringOneParameter(SqlDbType sqlDataType, IList<Literal> sqlDataTypeParameters)
{
string parameters = sqlDataTypeParameters != null ? ("(" + sqlDataTypeParameters[0] + ")") : "";
return sqlDataType + parameters;
}
private void HandleError(MessageHelper.MessageType errorMessage, Literal literal, string variableName, string sqlDbType, string cSharpType, string literalValue, int startLine, Exception exception)
{
if (IsCodeSenseRequest)
{
AddCodeSenseErrorItem(errorMessage, literal, literalValue, variableName, sqlDbType);
}
else
{
if (exception != null)
{
throw new ParameterizationFormatException(errorMessage, variableName, sqlDbType, cSharpType, literalValue, startLine, exception);
}
else
{
throw new ParameterizationFormatException(errorMessage, variableName, sqlDbType, cSharpType, literalValue, startLine);
}
}
}
private void AddCodeSenseErrorItem(MessageHelper.MessageType messageType, Literal literal, string literalValue, string variableName, string sqlDbType)
{
CodeSenseErrors.Add(new ScriptFileMarker
{
Level = ScriptFileMarkerLevel.Error,
Message = MessageHelper.GetLocalizedMessage(messageType, variableName, sqlDbType, literalValue),
ScriptRegion = new ScriptRegion
{
StartLineNumber = literal.StartLine,
StartColumnNumber = literal.StartColumn,
EndLineNumber = literal.StartLine,
EndColumnNumber = literal.StartColumn + literalValue.Length
}
});
}
private object ParseDateTime(string literalValue)
{
return DateTime.ParseExact(literalValue, SUPPORTED_ISO_DATE_TIME_FORMATS, CultureInfo.InvariantCulture, DateTimeStyles.None);
}
private object ParseDate(string literalValue)
{
return DateTime.ParseExact(literalValue, SUPPORTED_ISO_DATE_FORMATS, CultureInfo.InvariantCulture, DateTimeStyles.None);
}
private object ParseDateTimeOffset(string literalValue)
{
return DateTimeOffset.ParseExact(literalValue, SUPPORTED_ISO_DATE_TIME_OFFSET_FORMATS, CultureInfo.InvariantCulture, DateTimeStyles.None);
}
private bool ShouldParameterize(Literal literal)
{
switch (literal.LiteralType)
{
case LiteralType.Integer:
case LiteralType.Real:
case LiteralType.Money:
case LiteralType.Binary:
case LiteralType.String:
case LiteralType.Numeric:
return true;
default:
return false;
}
}
private void ExtractPrecisionAndScale(IList<Literal> dataTypeParameters, SqlParameter sqlParameter)
{
if (dataTypeParameters != null && dataTypeParameters.Count == 2)
{
Literal precisionLiteral = dataTypeParameters[0];
if (byte.TryParse(precisionLiteral.Value, out byte precision))
{
sqlParameter.Precision = precision;
}
Literal scaleLiteral = dataTypeParameters[1];
if (byte.TryParse(scaleLiteral.Value, out byte scale))
{
sqlParameter.Scale = scale;
}
}
}
private void ExtractPrecision(IList<Literal> dataTypeParameters, SqlParameter sqlParameter)
{
if (dataTypeParameters != null && dataTypeParameters.Count == 1)
{
Literal precisionLiteral = dataTypeParameters[0];
if (byte.TryParse(precisionLiteral.Value, out byte precision))
{
sqlParameter.Precision = precision;
}
}
}
private void ExtractSize(IList<Literal> dataTypeParameters, SqlParameter sqlParameter)
{
if (dataTypeParameters != null && dataTypeParameters.Count == 1)
{
Literal sizeLiteral = dataTypeParameters[0];
if (int.TryParse(sizeLiteral.Value, out int size))
{
sqlParameter.Size = size;
}
}
}
private object TryParseBinaryLiteral(string literalValue, string variableName, SqlDbType sqlDbType, int lineNumber)
{
if (literalValue.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
string hexString = literalValue.Substring(2);
return StringToByteArray(hexString);
}
throw new ParameterizationFormatException(MessageHelper.MessageType.BINARY_LITERAL_PREFIX_MISSING_ERROR, variableName, sqlDbType.ToString(), C_SHARP_BYTE_ARRAY, literalValue, lineNumber);
}
private string GetParameterName()
{
return "@p" + Guid.NewGuid().ToString("N"); //option N will give a guid without dashes
}
}
}

View File

@@ -0,0 +1,127 @@
//
// 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 System.Data.Common;
using System.IO;
using System.Linq;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Exceptions;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.AutoParameterizaition
{
public static class SqlParameterizer
{
private const int maxStringLength = 300000; // Approximately 600 Kb
private static readonly IList<ScriptFileMarker> EmptyCodeSenseItemList = Enumerable.Empty<ScriptFileMarker>().ToList();
public static SqlScriptGenerator GetScriptGenerator() => new Sql150ScriptGenerator();
public static TSqlParser GetTSqlParser(bool initialQuotedIdentifiers) => new TSql150Parser(initialQuotedIdentifiers);
/// <summary>
/// This method will parameterize the given SqlCommand.
/// Any single literal on the RHS of a declare statement will be parameterized
/// Any other literals will be ignored
/// </summary>
/// <param name="commandToParameterize">Command that will need to be parameterized</param>
public static void Parameterize(this DbCommand commandToParameterize)
{
TSqlFragment rootFragment = GetAbstractSyntaxTree(commandToParameterize);
TsqlMultiVisitor multiVisitor = new TsqlMultiVisitor(isCodeSenseRequest: false); // Use the vistor pattern to examine the parse tree
rootFragment.AcceptChildren(multiVisitor); // Now walk the tree
//reformat and validate the transformed command
SqlScriptGenerator scriptGenerator = GetScriptGenerator();
scriptGenerator.GenerateScript(rootFragment, out string formattedSQL);
if (!string.IsNullOrEmpty(formattedSQL))
{
commandToParameterize.CommandText = formattedSQL;
}
commandToParameterize.Parameters.AddRange(multiVisitor.Parameters.ToArray());
multiVisitor.Reset();
}
/// <summary>
/// Parses the given script to provide message, warning, error.
/// </summary>
/// <param name="scriptToParse">Script that will be parsed</param>
/// <param name="telemetryManager">Used to emit telemetry events</param>
/// <returns></returns>
public static IList<ScriptFileMarker> CodeSense(string scriptToParse)
{
if (scriptToParse == null)
{
return EmptyCodeSenseItemList;
}
int CurrentScriptlength = scriptToParse.Length;
if (CurrentScriptlength > maxStringLength)
{
ScriptFileMarker maxStringLengthCodeSenseItem = new ScriptFileMarker
{
Level = ScriptFileMarkerLevel.Error,
Message = SR.ScriptTooLarge(maxStringLength, CurrentScriptlength),
ScriptRegion = new ScriptRegion
{
// underline first row in the text
StartLineNumber = 1,
StartColumnNumber = 1,
EndLineNumber = 2,
EndColumnNumber = 1
}
};
return new ScriptFileMarker[] { maxStringLengthCodeSenseItem };
}
TSqlFragment rootFragment = GetAbstractSyntaxTree(scriptToParse);
TsqlMultiVisitor multiVisitor = new TsqlMultiVisitor(isCodeSenseRequest: true); // Use the vistor pattern to examine the parse tree
rootFragment.AcceptChildren(multiVisitor); // Now walk the tree
if (multiVisitor.CodeSenseErrors != null && multiVisitor.CodeSenseErrors.Count != 0)
{
multiVisitor.CodeSenseMessages.AddRange(multiVisitor.CodeSenseErrors);
}
return multiVisitor.CodeSenseMessages;
}
private static TSqlFragment GetAbstractSyntaxTree(DbCommand commandToParameterize)
{
// Capture the current CommandText in a format that the parser can work with
string commandText = commandToParameterize.CommandText;
int currentScriptLength = commandText.Length;
if (currentScriptLength > maxStringLength)
{
throw new ParameterizationScriptTooLargeException(currentScriptLength, errorMessage: SR.ScriptTooLarge(maxStringLength, currentScriptLength));
}
return GetAbstractSyntaxTree(commandText);
}
private static TSqlFragment GetAbstractSyntaxTree(string script)
{
using (TextReader textReader = new StringReader(script))
{
TSqlParser parser = GetTSqlParser(true);
TSqlFragment rootFragment = parser.Parse(textReader, out IList<ParseError> parsingErrors); // Get the parse tree
// if we could not parse the SQL we will throw an exception. Better here than on the server
if (parsingErrors.Count > 0)
{
throw new ParameterizationParsingException(parsingErrors[0].Line, parsingErrors[0].Column, parsingErrors[0].Message);
}
return rootFragment;
}
}
}
}

View File

@@ -0,0 +1,170 @@
//
// 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 System.Text;
using Microsoft.Data.SqlClient;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.AutoParameterizaition
{
/// <summary>
/// Entry point for SqlParameterization, this class is responsible for visiting the parse tree and identifying the scalar expressions to be parameterized
/// </summary>
internal class TsqlMultiVisitor : TSqlFragmentVisitor
{
private readonly ScalarExpressionTransformer ScalarExpressionTransformer;
private readonly bool IsCodeSenseRequest;
private Dictionary<string, int> _executionParameters = null;
public List<SqlParameter> Parameters { get; private set; }
public List<ScriptFileMarker> CodeSenseMessages { get; private set; }
public List<ScriptFileMarker> CodeSenseErrors { get; private set; }
public Dictionary<string, int> ExecutionParameters
{
get
{
if (_executionParameters == null)
{
_executionParameters = new Dictionary<string, int>();
}
return _executionParameters;
}
}
public TsqlMultiVisitor(bool isCodeSenseRequest)
{
Parameters = new List<SqlParameter>();
IsCodeSenseRequest = isCodeSenseRequest;
CodeSenseMessages = new List<ScriptFileMarker>();
CodeSenseErrors = new List<ScriptFileMarker>();
ScalarExpressionTransformer = new ScalarExpressionTransformer(isCodeSenseRequest, CodeSenseErrors);
}
public override void ExplicitVisit(DeclareVariableStatement node)
{
if (node == null || node.Declarations == null)
{
return;
}
StringBuilder codeSenseMessageStringBuilder = new StringBuilder();
int endLine = -1;
int endCol = -1;
foreach (DeclareVariableElement declareVariableElement in node.Declarations)
{
if (declareVariableElement.DataType is SqlDataTypeReference dataTypeReference)
{
SqlDataTypeOption sqlDataTypeOption = dataTypeReference.SqlDataTypeOption;
if (ShouldParamterize(sqlDataTypeOption))
{
IList<Literal> sqlDataTypeParameters = dataTypeReference.Parameters;
ScalarExpressionTransformer.SqlDataTypeOption = sqlDataTypeOption;
ScalarExpressionTransformer.SqlDataTypeParameters = sqlDataTypeParameters;
ScalarExpressionTransformer.VariableName = declareVariableElement.VariableName.Value;
ScalarExpression declareVariableElementValue = declareVariableElement.Value;
ScalarExpressionTransformer.ExplicitVisit(declareVariableElementValue);
declareVariableElement.Value = ScalarExpressionTransformer.GetTransformedExpression();
IList<SqlParameter> sqlParameters = ScalarExpressionTransformer.Parameters;
if (sqlParameters.Count == 1 && declareVariableElementValue != null)
{
codeSenseMessageStringBuilder.Append(SR.ParameterizationDetails(declareVariableElement.VariableName.Value,
sqlParameters[0].SqlDbType.ToString(),
sqlParameters[0].Size,
sqlParameters[0].Precision,
sqlParameters[0].Scale,
sqlParameters[0].SqlValue.ToString()));
endLine = declareVariableElementValue.StartLine;
endCol = declareVariableElementValue.StartColumn + declareVariableElementValue.FragmentLength;
if (!IsCodeSenseRequest)
{
string sqlParameterKey = sqlParameters[0].SqlDbType.ToString();
ExecutionParameters.TryGetValue(sqlParameterKey, out int currentCount);
ExecutionParameters[sqlParameterKey] = currentCount + 1;
}
}
Parameters.AddRange(sqlParameters);
ScalarExpressionTransformer.Reset();
}
}
}
if (codeSenseMessageStringBuilder.Length > 0)
{
CodeSenseMessages.Add(new ScriptFileMarker
{
Level = ScriptFileMarkerLevel.Information,
Message = codeSenseMessageStringBuilder.ToString(),
ScriptRegion = new ScriptRegion
{
StartLineNumber = node.StartLine,
StartColumnNumber = node.StartColumn,
EndLineNumber = endLine == -1 ? node.StartLine : endLine,
EndColumnNumber = endCol == -1 ? node.StartColumn + node.LastTokenIndex - node.FirstTokenIndex : endCol
}
});
}
node.AcceptChildren(this);
base.ExplicitVisit(node); // let the base class finish up
}
private bool ShouldParamterize(SqlDataTypeOption sqlDataTypeOption)
{
switch (sqlDataTypeOption)
{
case SqlDataTypeOption.BigInt:
case SqlDataTypeOption.Int:
case SqlDataTypeOption.SmallInt:
case SqlDataTypeOption.TinyInt:
case SqlDataTypeOption.Bit:
case SqlDataTypeOption.Decimal:
case SqlDataTypeOption.Numeric:
case SqlDataTypeOption.Money:
case SqlDataTypeOption.SmallMoney:
case SqlDataTypeOption.Float:
case SqlDataTypeOption.Real:
case SqlDataTypeOption.DateTime:
case SqlDataTypeOption.SmallDateTime:
case SqlDataTypeOption.Char:
case SqlDataTypeOption.VarChar:
case SqlDataTypeOption.NChar:
case SqlDataTypeOption.NVarChar:
case SqlDataTypeOption.Binary:
case SqlDataTypeOption.VarBinary:
case SqlDataTypeOption.UniqueIdentifier:
case SqlDataTypeOption.Date:
case SqlDataTypeOption.Time:
case SqlDataTypeOption.DateTime2:
case SqlDataTypeOption.DateTimeOffset:
return true;
default:
return false;
}
}
public void Reset()
{
Parameters.Clear();
CodeSenseMessages.Clear();
CodeSenseErrors.Clear();
ExecutionParameters.Clear();
}
}
}

View File

@@ -22,6 +22,7 @@ using Microsoft.SqlServer.Management.SqlParser.Parser;
using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
@@ -790,6 +791,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
try
{
bool oldEnableIntelliSense = oldSettings.SqlTools.IntelliSense.EnableIntellisense;
bool oldAlwaysEncryptedParameterizationEnabled = oldSettings.SqlTools.QueryExecutionSettings.IsAlwaysEncryptedParameterizationEnabled;
bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableErrorChecking;
// update the current settings to reflect any changes
@@ -797,13 +799,12 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// if script analysis settings have changed we need to clear the current diagnostic markers
if (oldEnableIntelliSense != newSettings.SqlTools.IntelliSense.EnableIntellisense
|| oldEnableDiagnostics != newSettings.SqlTools.IntelliSense.EnableErrorChecking)
|| oldEnableDiagnostics != newSettings.SqlTools.IntelliSense.EnableErrorChecking
|| oldAlwaysEncryptedParameterizationEnabled != newSettings.SqlTools.QueryExecutionSettings.IsAlwaysEncryptedParameterizationEnabled)
{
// if the user just turned off diagnostics then send an event to clear the error markers
if (!newSettings.IsDiagnosticsEnabled)
{
ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0];
foreach (var scriptFile in CurrentWorkspace.GetOpenedFiles())
{
await DiagnosticsHelper.ClearScriptDiagnostics(scriptFile.ClientUri, eventContext);
@@ -812,7 +813,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// otherwise rerun diagnostic analysis on all opened SQL files
else
{
await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext);
await RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext);
}
}
}
@@ -1698,6 +1699,11 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
}
if (CurrentWorkspaceSettings.QueryExecutionSettings.IsAlwaysEncryptedParameterizationEnabled)
{
markers.AddRange(SqlParameterizer.CodeSense(scriptFile.Contents));
}
return markers.ToArray();
}

View File

@@ -3068,6 +3068,41 @@ namespace Microsoft.SqlTools.ServiceLayer
return Keys.GetString(Keys.QueryServiceSaveAsFail, fileName, message);
}
public static string ParameterizationDetails(string variableName, string sqlDbType, int size, int precision, int scale, string sqlValue)
{
return Keys.GetString(Keys.ParameterizationDetails, variableName, sqlDbType, size, precision, scale, sqlValue);
}
public static string ErrorMessageHeader(int lineNumber)
{
return Keys.GetString(Keys.ErrorMessageHeader, lineNumber);
}
public static string ErrorMessage(string variableName, string sqlDataType, string literalValue)
{
return Keys.GetString(Keys.ErrorMessage, variableName, sqlDataType, literalValue);
}
public static string DateTimeErrorMessage(string variableName, string sqlDataType, string literalValue)
{
return Keys.GetString(Keys.DateTimeErrorMessage, variableName, sqlDataType, literalValue);
}
public static string BinaryLiteralPrefixMissingError(string variableName, string sqlDataType, string literalValue)
{
return Keys.GetString(Keys.BinaryLiteralPrefixMissingError, variableName, sqlDataType, literalValue);
}
public static string ParsingErrorHeader(int lineNumber, int columnNumber)
{
return Keys.GetString(Keys.ParsingErrorHeader, lineNumber, columnNumber);
}
public static string ScriptTooLarge(int maxChars, int currentChars)
{
return Keys.GetString(Keys.ScriptTooLarge, maxChars, currentChars);
}
public static string SerializationServiceUnsupportedFormat(string formatName)
{
return Keys.GetString(Keys.SerializationServiceUnsupportedFormat, formatName);
@@ -3366,6 +3401,27 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string SqlCmdUnsupportedToken = "SqlCmdUnsupportedToken";
public const string ParameterizationDetails = "ParameterizationDetails";
public const string ErrorMessageHeader = "ErrorMessageHeader";
public const string ErrorMessage = "ErrorMessage";
public const string DateTimeErrorMessage = "DateTimeErrorMessage";
public const string BinaryLiteralPrefixMissingError = "BinaryLiteralPrefixMissingError";
public const string ParsingErrorHeader = "ParsingErrorHeader";
public const string ScriptTooLarge = "ScriptTooLarge";
public const string SerializationServiceUnsupportedFormat = "SerializationServiceUnsupportedFormat";
@@ -4461,6 +4517,12 @@ namespace Microsoft.SqlTools.ServiceLayer
}
public static string GetString(string key, object arg0, object arg1, object arg2)
{
return string.Format(global::System.Globalization.CultureInfo.CurrentCulture, resourceManager.GetString(key, _culture), arg0, arg1, arg2);
}
public static string GetString(string key, object arg0, object arg1, object arg2, object arg3)
{
return string.Format(global::System.Globalization.CultureInfo.CurrentCulture, resourceManager.GetString(key, _culture), arg0, arg1, arg2, arg3);

View File

@@ -348,6 +348,41 @@
<value>Encountered unsupported token {0}</value>
<comment></comment>
</data>
<data name="ParameterizationDetails" xml:space="preserve">
<value>{0} will be converted to a Microsoft.Data.SqlClient.SqlParameter object with the following properties: SqlDbType = {1}, Size = {2}, Precision = {3}, Scale = {4}, SqlValue = {5}</value>
<comment>.
Parameters: 0 - variableName (string), 1 - sqlDbType (string), 2 - size (int), 3 - precision (int), 4 - scale (int), 5 - sqlValue (string) </comment>
</data>
<data name="ErrorMessageHeader" xml:space="preserve">
<value>Line {0}</value>
<comment>.
Parameters: 0 - lineNumber (int) </comment>
</data>
<data name="ErrorMessage" xml:space="preserve">
<value>Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType). Literal value: {2} </value>
<comment>.
Parameters: 0 - variableName (string), 1 - sqlDataType (string), 2 - literalValue (string) </comment>
</data>
<data name="DateTimeErrorMessage" xml:space="preserve">
<value>Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as it used an unsupported date/time format. Use one of the supported date/time formats. Literal value: {2}</value>
<comment>.
Parameters: 0 - variableName (string), 1 - sqlDataType (string), 2 - literalValue (string) </comment>
</data>
<data name="BinaryLiteralPrefixMissingError" xml:space="preserve">
<value>Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as prefix 0x is expected for a binary literals. Literal value: {2} </value>
<comment>.
Parameters: 0 - variableName (string), 1 - sqlDataType (string), 2 - literalValue (string) </comment>
</data>
<data name="ParsingErrorHeader" xml:space="preserve">
<value>Line {0}, column {1}</value>
<comment>.
Parameters: 0 - lineNumber (int), 1 - columnNumber (int) </comment>
</data>
<data name="ScriptTooLarge" xml:space="preserve">
<value>The current script is too large for Parameterization for Always Encrypted, please disable Parameterization for Always Encrypted in Query Options (Query &gt; Query Options &gt; Execution &gt; Advanced). Maximum allowable length: {0} characters, Current script length: {1} characters</value>
<comment>.
Parameters: 0 - maxChars (int), 1 - currentChars (int) </comment>
</data>
<data name="SerializationServiceUnsupportedFormat" xml:space="preserve">
<value>Unsupported Save Format: {0}</value>
<comment>.

View File

@@ -150,6 +150,22 @@ SqlCmdExitOnError = An error was encountered during execution of batch. Exiting.
SqlCmdUnsupportedToken = Encountered unsupported token {0}
### AutoParameterization for Always Encrypted strings
ParameterizationDetails (string variableName, string sqlDbType, int size, int precision, int scale, string sqlValue) = {0} will be converted to a Microsoft.Data.SqlClient.SqlParameter object with the following properties: SqlDbType = {1}, Size = {2}, Precision = {3}, Scale = {4}, SqlValue = {5}
ErrorMessageHeader(int lineNumber) = Line {0}
ErrorMessage (string variableName, string sqlDataType, string literalValue) = Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType). Literal value: {2}
DateTimeErrorMessage (string variableName, string sqlDataType, string literalValue) = Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as it used an unsupported date/time format. Use one of the supported date/time formats. Literal value: {2}
BinaryLiteralPrefixMissingError (string variableName, string sqlDataType, string literalValue) = Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as prefix 0x is expected for a binary literals. Literal value: {2}
ParsingErrorHeader (int lineNumber, int columnNumber) = Line {0}, column {1}
ScriptTooLarge (int maxChars, int currentChars) = The current script is too large for Parameterization for Always Encrypted, please disable Parameterization for Always Encrypted in Query Options (Query > Query Options > Execution > Advanced). Maximum allowable length: {0} characters, Current script length: {1} characters
############################################################################
# Serialization Service

View File

@@ -2056,11 +2056,6 @@
<target state="new">Encountered unsupported token {0}</target>
<note></note>
</trans-unit>
<trans-unit id="SqlAssessmentOperationExecuteCalledTwice">
<source>A SQL Assessment operation's Execute method should not be called more than once</source>
<target state="new">A SQL Assessment operation's Execute method should not be called more than once</target>
<note></note>
</trans-unit>
<trans-unit id="SqlAssessmentGenerateScriptTaskName">
<source>Generate SQL Assessment script</source>
<target state="new">Generate SQL Assessment script</target>
@@ -2081,6 +2076,48 @@
<target state="new">Unsupported engine edition {0}</target>
<note>.
Parameters: 0 - editionCode (int) </note>
</trans-unit>
<trans-unit id="ParameterizationDetails">
<source>{0} will be converted to a Microsoft.Data.SqlClient.SqlParameter object with the following properties: SqlDbType = {1}, Size = {2}, Precision = {3}, Scale = {4}, SqlValue = {5}</source>
<target state="new">{0} will be converted to a Microsoft.Data.SqlClient.SqlParameter object with the following properties: SqlDbType = {1}, Size = {2}, Precision = {3}, Scale = {4}, SqlValue = {5}</target>
<note>.
Parameters: 0 - variableName (string), 1 - sqlDbType (string), 2 - size (int), 3 - precision (int), 4 - scale (int), 5 - sqlValue (string) </note>
</trans-unit>
<trans-unit id="ErrorMessageHeader">
<source>Line {0}</source>
<target state="new">Line {0}</target>
<note>.
Parameters: 0 - lineNumber (int) </note>
</trans-unit>
<trans-unit id="ErrorMessage">
<source>Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType). Literal value: {2} </source>
<target state="new">Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType). Literal value: {2} </target>
<note>.
Parameters: 0 - variableName (string), 1 - sqlDataType (string), 2 - literalValue (string) </note>
</trans-unit>
<trans-unit id="DateTimeErrorMessage">
<source>Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as it used an unsupported date/time format. Use one of the supported date/time formats. Literal value: {2}</source>
<target state="new">Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as it used an unsupported date/time format. Use one of the supported date/time formats. Literal value: {2}</target>
<note>.
Parameters: 0 - variableName (string), 1 - sqlDataType (string), 2 - literalValue (string) </note>
</trans-unit>
<trans-unit id="BinaryLiteralPrefixMissingError">
<source>Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as prefix 0x is expected for a binary literals. Literal value: {2} </source>
<target state="new">Unable to convert {0} to a Microsoft.Data.SqlClient.SqlParameter object. The specified literal cannot be converted to {1}(Microsoft.Data.SqlDbType), as prefix 0x is expected for a binary literals. Literal value: {2} </target>
<note>.
Parameters: 0 - variableName (string), 1 - sqlDataType (string), 2 - literalValue (string) </note>
</trans-unit>
<trans-unit id="ParsingErrorHeader">
<source>Line {0}, column {1}</source>
<target state="new">Line {0}, column {1}</target>
<note>.
Parameters: 0 - lineNumber (int), 1 - columnNumber (int) </note>
</trans-unit>
<trans-unit id="ScriptTooLarge">
<source>The current script is too large for Parameterization for Always Encrypted, please disable Parameterization for Always Encrypted in Query Options (Query &gt; Query Options &gt; Execution &gt; Advanced). Maximum allowable length: {0} characters, Current script length: {1} characters</source>
<target state="new">The current script is too large for Parameterization for Always Encrypted, please disable Parameterization for Always Encrypted in Query Options (Query &gt; Query Options &gt; Execution &gt; Advanced). Maximum allowable length: {0} characters, Current script length: {1} characters</target>
<note>.
Parameters: 0 - maxChars (int), 1 - currentChars (int) </note>
</trans-unit>
<trans-unit id="ProjectExtractTaskName">
<source>Extract project files</source>

View File

@@ -19,6 +19,9 @@ using System.Globalization;
using System.Collections.ObjectModel;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.BatchParser;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
{
@@ -399,6 +402,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
dbCommand.CommandType = CommandType.Text;
dbCommand.CommandTimeout = 0;
if (WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.QueryExecutionSettings.IsAlwaysEncryptedParameterizationEnabled)
{
dbCommand.Parameterize();
}
List<DbColumn[]> columnSchemas = null;
if (getFullColumnSchema)
{

View File

@@ -6,6 +6,7 @@
using System;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.Utility;
using Newtonsoft.Json;
namespace Microsoft.SqlTools.ServiceLayer.SqlContext
{
@@ -168,6 +169,11 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
/// </summary>
private bool DefaultSqlCmdMode = false;
/// <summary>
/// Default value for flag to enable Always Encrypted Parameterization
/// </summary>
private bool DefaultAlwaysEncryptedParameterizationValue = false;
#endregion
#region Member Variables
@@ -653,6 +659,22 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
}
}
/// <summary>
/// Set Always Encrypted Parameterization Mode
/// </summary>
[JsonProperty("alwaysEncryptedParameterization")]
public bool IsAlwaysEncryptedParameterizationEnabled
{
get
{
return GetOptionValue<bool>("alwaysEncryptedParameterization", DefaultAlwaysEncryptedParameterizationValue);
}
set
{
SetOptionValue("alwaysEncryptedParameterization", value);
}
}
#endregion
#region Public Methods
@@ -691,6 +713,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
AnsiWarnings = newSettings.AnsiWarnings;
AnsiNulls = newSettings.AnsiNulls;
IsSqlCmdMode = newSettings.IsSqlCmdMode;
IsAlwaysEncryptedParameterizationEnabled = newSettings.IsAlwaysEncryptedParameterizationEnabled;
}
#endregion

View File

@@ -93,6 +93,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
if (settings != null)
{
this.SqlTools.IntelliSense.Update(settings.SqlTools.IntelliSense);
this.SqlTools.QueryExecutionSettings.Update(settings.SqlTools.QueryExecutionSettings);
}
}

View File

@@ -0,0 +1,273 @@
//
// 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.Common;
using System.Diagnostics;
using System.Linq;
using Microsoft.Data.SqlClient;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition;
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition.Exceptions;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using Xunit;
using static System.Linq.Enumerable;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.AutoParameterization
{
/// <summary>
/// Parameterization for Always Encrypted is a feature that automatically converts Transact-SQL variables
/// into query parameters (instances of <c>SqlParameter</c> Class). This allows the underlying .NET Framework
/// Data Provider for SQL Server to detect data targeting encrypted columns, and to encrypt such data before
/// sending it to the database.
/// Without parameterization, the .NET Framework Data Provider passes each statement in the Query Editor,
/// as a non-parameterized query. If the query contains literals or Transact-SQL variables that target encrypted columns,
/// the.NET Framework Data Provider for SQL Server won't be able to detect and encrypt them, before sending the query to the database.
/// As a result, the query will fail due to type mismatch (between the plaintext literal Transact-SQL variable and the encrypted column).
/// This class unit-tests the functionality os the Parameterization for Always Encrypted feature.
/// </summary>
public class SqlParameterizerTests
{
#region Query Parameterization Tests
/// <summary>
/// SqlParameterizer should parameterize Transact-SQL variables that meet the following pre-requisite conditions:
/// - Are declared and initialized in the same statement(inline initialization).
/// - Are initialized using a single literal.
/// </summary>
[Fact]
public void SqlParameterizerShouldParameterizeValidVariables()
{
const string ssn = "795-73-9838";
const string birthday = "19990104";
const string salary = "$30000";
string sql = $@"
DECLARE @SSN CHAR(11) = '{ssn}'
DECLARE @BIRTHDAY DATE = '{birthday}'
DECLARE @SALARY MONEY = '{salary}'
SELECT * FROM [dbo].[Patients]
WHERE [SSN] = @SSN AND [BIRTHDAY] = @BIRTHDAY AND [SALARY] = @SALARY";
DbCommand command = new SqlCommand { CommandText = sql };
command.Parameterize();
Assert.Equal(expected: 3, actual: command.Parameters.Count);
}
/// <summary>
/// SqlParameterizer should not attempt parameterize Transact-SQL variables that do not meet the following pre-requisite conditions:
/// - Are declared and initialized in the same statement(inline initialization).
/// - Are initialized using a single literal.
/// The first variable has initialization separate from declaration and so should not be parameterized.
/// The second is using a function used instead of a literal and so should not be parameterized.
/// The third is using an expression used instead of a literal and so should not be parameterized.
/// </summary>
[Fact]
public void SqlParameterizerShouldNotParameterizeInvalidVariables()
{
string sql = $@"
DECLARE @Name nvarchar(50);
SET @Name = 'Abel';
DECLARE @StartDate date = GETDATE();
DECLARE @NewSalary money = @Salary * 1.1;
SELECT * FROM [dbo].[Patients]
WHERE [Name] = @Name AND [StartDate] = @StartDate AND [NewSalary] = @NewSalary";
DbCommand command = new SqlCommand { CommandText = sql };
command.Parameterize();
Assert.Equal(expected: 0, actual: command.Parameters.Count);
}
/// <summary>
/// SQLDOM parser currently cannot handle very large scripts and runs out of memory.
/// Batch statements larger than 300000 characters (Approximately 600 Kb) should
/// throw <c>ParameterizationScriptTooLargeException</c>.
/// </summary>
[Fact]
public void SqlParameterizerShouldThrowWhenSqlIsTooLong()
{
string sqlLength_300 = $@"
DECLARE @SSN CHAR(11) = '123-45-6789'
DECLARE @BIRTHDAY DATE = '19990104'
DECLARE @SALARY MONEY = '$30000'
SELECT * FROM [dbo].[Patients]
WHERE [N] = @SSN AND [B] = @BIRTHDAY AND [S] = @SALARY
GO";
// SQL less than or equal to 300000 should pass
string smallSql = string.Concat(Repeat(element: sqlLength_300, count: 1000));
DbCommand command1 = new SqlCommand { CommandText = smallSql };
command1.Parameterize();
// SQL greater than 300000 characters should throw
string bigSql = string.Concat(Repeat(element: sqlLength_300, count: 1100));
DbCommand command2 = new SqlCommand { CommandText = bigSql };
Assert.Throws<ParameterizationScriptTooLargeException>(() => command2.Parameterize());
}
/// <summary>
/// During parameterization, if we could not parse the SQL we will throw an <c>ParameterizationParsingException</c>.
/// Better to catch the error here than on the server.
/// </summary>
[Fact]
public void SqlParameterizerShouldThrowWhenSqlIsInvalid()
{
string invalidSql = "THIS IS INVALID SQL";
string sql = string.Concat(Repeat(element: invalidSql, count: 1000));
DbCommand command = new SqlCommand { CommandText = sql };
Assert.Throws<ParameterizationParsingException>(() => command.Parameterize());
}
/// <summary>
/// While the SqlParameterizer should parameterize Transact-SQL variables that are declared and initialized
/// in the same statement(inline initialization) and are initialized using a single literal, the type of the
/// literal used for the initialization of the variable must also match the type in the variable declaration.
/// If not, a <c>ParameterizationFormatException</c> should get thrown.
/// </summary>
[Fact]
public void SqlParameterizerShouldThrowWhenLiteralHasTypeMismatch()
{
// variable is declared an int but is getting set to character data
string sql = $@"
DECLARE @Number int = 'ABCDEFG'
SELECT * FROM [dbo].[Table]
WHERE [N] = @Number
GO";
DbCommand command = new SqlCommand { CommandText = sql };
Assert.Throws<ParameterizationFormatException>(() => command.Parameterize());
}
/// <summary>
/// A side effect of the parameterization process is that, when a batch script was composed
/// entirely of comments, the comments were stripped away and the <c>CommandText</c>
/// property of the <c>DbCommand</c> would be replaced with an empty string. When this happens,
/// the DbCommand object will throw an exception with the following message:
/// BeginExecuteReader: CommandText property has not been initialized
/// </summary>
[Fact]
public void CommentOnlyBatchesShouldNotBeErasedFromCommandText()
{
string sql = $@"
-- ALTER TABLE BatchParameterization
-- ALTER COLUMN
-- [unique_key] [UNIQUEIDENTIFIER] NOT NULL";
DbCommand command = new SqlCommand { CommandText = sql };
command.Parameterize();
Assert.False(string.IsNullOrEmpty(command.CommandText));
Assert.Equal(expected: sql, actual: command.CommandText);
}
#endregion
#region Prarmeterization Codesense Tests
/// <summary>
/// When requesting a collection of <c>ScriptFileMarker</c> by calling the <c>SqlParameterizer.CodeSense</c>
/// method, if a null script is passed in, the reuslt should be an empty collection.
/// </summary>
[Fact]
public void CodeSenseShouldReturnEmptyListWhenGivenANullScript()
{
string sql = null;
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
Assert.NotNull(result);
Assert.Empty(result);
}
/// <summary>
/// When requesting a collection of <c>ScriptFileMarker</c> by calling the <c>SqlParameterizer.CodeSense</c>
/// method, if a script is passed in that contains no valid parameters, the reuslt should be an empty collection.
/// </summary>
[Fact]
public void CodeSenseShouldReturnEmptyListWhenGivenAParameterlessScript()
{
// SQL with no parameters
string sql = $@"
SELECT * FROM [dbo].[Patients]
WHERE [N] = @SSN AND [B] = @BIRTHDAY AND [S] = @SALARY
GO";
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
Assert.NotNull(result);
Assert.Empty(result);
}
/// <summary>
/// SQLDOM parser currently cannot handle very large scripts and runs out of memory.
/// SQL statements larger than 300000 characters (Approximately 600 Kb) should
/// return a max string sength code sense item. These will be returned to ADS to display to the user as intelli-sense.
/// </summary>
[Fact]
public void CodeSenseShouldReturnMaxStringLengthScriptFileMarkerErrorItemWhenScriptIsTooLong()
{
// SQL length of 300 characters
string sqlLength_300 = $@"
DECLARE @SSN CHAR(11) = '123-45-6789'
DECLARE @BIRTHDAY DATE = '19990104'
DECLARE @SALARY MONEY = '$30000'
SELECT * FROM [dbo].[Patients]
WHERE [N] = @SSN AND [B] = @BIRTHDAY AND [S] = @SALARY
GO";
// Repeat the SQL 1001 times to exceed length threshold
string sql = string.Concat(Repeat(element: sqlLength_300, count: 1100));
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
string expectedMessage = SR.ScriptTooLarge(maxChars: 300000, currentChars: sql.Length);
Console.WriteLine(result[0].Message);
Assert.NotEmpty(result);
Assert.Equal(expected: 1, actual: result.Count);
Assert.Equal(expected: ScriptFileMarkerLevel.Error, actual: result[0].Level);
Assert.Equal(expected: expectedMessage, actual: result[0].Message);
}
/// <summary>
/// When requesting a collection of <c>ScriptFileMarker</c> by calling the <c>SqlParameterizer.CodeSense</c>
/// method, if a script is passed in that contains 3 valid parameters, the reuslt should be a collection of
/// three informational code sense items. These will be returned to ADS to display to the user as intelli-sense.
/// </summary>
[Fact]
public void CodeSenseShouldReturnInformationalCodeSenseItemsForValidParameters()
{
const string ssn = "795-73-9838";
const string birthday = "19990104";
const string salary = "$30000";
string sql = $@"
DECLARE @SSN CHAR(11) = '{ssn}'
DECLARE @BIRTHDAY DATE = '{birthday}'
DECLARE @SALARY MONEY = '{salary}'
SELECT * FROM [dbo].[Patients]
WHERE [SSN] = @SSN AND [BIRTHDAY] = @BIRTHDAY AND [SALARY] = @SALARY";
IList<ScriptFileMarker> result = SqlParameterizer.CodeSense(sql);
Assert.NotEmpty(result);
Assert.Equal(expected: 3, actual: result.Count);
Assert.True(Enumerable.All(result, i => i.Level == ScriptFileMarkerLevel.Information));
}
#endregion
}
}