mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-08 17:25:08 -05:00
Add Always Encrypted Parameterization Functionality (#953)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 > Query Options > Execution > 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>.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 > Query Options > Execution > 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 > Query Options > Execution > 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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user