mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-17 02:51:45 -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.SqlServer.Management.SqlParser.SqlCodeDom;
|
||||||
using Microsoft.SqlTools.Extensibility;
|
using Microsoft.SqlTools.Extensibility;
|
||||||
using Microsoft.SqlTools.Hosting.Protocol;
|
using Microsoft.SqlTools.Hosting.Protocol;
|
||||||
|
using Microsoft.SqlTools.ServiceLayer.AutoParameterizaition;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||||
@@ -790,6 +791,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool oldEnableIntelliSense = oldSettings.SqlTools.IntelliSense.EnableIntellisense;
|
bool oldEnableIntelliSense = oldSettings.SqlTools.IntelliSense.EnableIntellisense;
|
||||||
|
bool oldAlwaysEncryptedParameterizationEnabled = oldSettings.SqlTools.QueryExecutionSettings.IsAlwaysEncryptedParameterizationEnabled;
|
||||||
bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableErrorChecking;
|
bool? oldEnableDiagnostics = oldSettings.SqlTools.IntelliSense.EnableErrorChecking;
|
||||||
|
|
||||||
// update the current settings to reflect any changes
|
// 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 script analysis settings have changed we need to clear the current diagnostic markers
|
||||||
if (oldEnableIntelliSense != newSettings.SqlTools.IntelliSense.EnableIntellisense
|
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 the user just turned off diagnostics then send an event to clear the error markers
|
||||||
if (!newSettings.IsDiagnosticsEnabled)
|
if (!newSettings.IsDiagnosticsEnabled)
|
||||||
{
|
{
|
||||||
ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0];
|
|
||||||
|
|
||||||
foreach (var scriptFile in CurrentWorkspace.GetOpenedFiles())
|
foreach (var scriptFile in CurrentWorkspace.GetOpenedFiles())
|
||||||
{
|
{
|
||||||
await DiagnosticsHelper.ClearScriptDiagnostics(scriptFile.ClientUri, eventContext);
|
await DiagnosticsHelper.ClearScriptDiagnostics(scriptFile.ClientUri, eventContext);
|
||||||
@@ -812,7 +813,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
|||||||
// otherwise rerun diagnostic analysis on all opened SQL files
|
// otherwise rerun diagnostic analysis on all opened SQL files
|
||||||
else
|
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();
|
return markers.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3068,6 +3068,41 @@ namespace Microsoft.SqlTools.ServiceLayer
|
|||||||
return Keys.GetString(Keys.QueryServiceSaveAsFail, fileName, message);
|
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)
|
public static string SerializationServiceUnsupportedFormat(string formatName)
|
||||||
{
|
{
|
||||||
return Keys.GetString(Keys.SerializationServiceUnsupportedFormat, formatName);
|
return Keys.GetString(Keys.SerializationServiceUnsupportedFormat, formatName);
|
||||||
@@ -3366,6 +3401,27 @@ namespace Microsoft.SqlTools.ServiceLayer
|
|||||||
public const string SqlCmdUnsupportedToken = "SqlCmdUnsupportedToken";
|
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";
|
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)
|
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);
|
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>
|
<value>Encountered unsupported token {0}</value>
|
||||||
<comment></comment>
|
<comment></comment>
|
||||||
</data>
|
</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">
|
<data name="SerializationServiceUnsupportedFormat" xml:space="preserve">
|
||||||
<value>Unsupported Save Format: {0}</value>
|
<value>Unsupported Save Format: {0}</value>
|
||||||
<comment>.
|
<comment>.
|
||||||
|
|||||||
@@ -150,6 +150,22 @@ SqlCmdExitOnError = An error was encountered during execution of batch. Exiting.
|
|||||||
|
|
||||||
SqlCmdUnsupportedToken = Encountered unsupported token {0}
|
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
|
# Serialization Service
|
||||||
|
|
||||||
|
|||||||
@@ -2056,11 +2056,6 @@
|
|||||||
<target state="new">Encountered unsupported token {0}</target>
|
<target state="new">Encountered unsupported token {0}</target>
|
||||||
<note></note>
|
<note></note>
|
||||||
</trans-unit>
|
</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">
|
<trans-unit id="SqlAssessmentGenerateScriptTaskName">
|
||||||
<source>Generate SQL Assessment script</source>
|
<source>Generate SQL Assessment script</source>
|
||||||
<target state="new">Generate SQL Assessment script</target>
|
<target state="new">Generate SQL Assessment script</target>
|
||||||
@@ -2081,6 +2076,48 @@
|
|||||||
<target state="new">Unsupported engine edition {0}</target>
|
<target state="new">Unsupported engine edition {0}</target>
|
||||||
<note>.
|
<note>.
|
||||||
Parameters: 0 - editionCode (int) </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>
|
||||||
<trans-unit id="ProjectExtractTaskName">
|
<trans-unit id="ProjectExtractTaskName">
|
||||||
<source>Extract project files</source>
|
<source>Extract project files</source>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ using System.Globalization;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||||
using Microsoft.SqlTools.ServiceLayer.BatchParser;
|
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
|
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
||||||
{
|
{
|
||||||
@@ -399,6 +402,11 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
|
|||||||
dbCommand.CommandType = CommandType.Text;
|
dbCommand.CommandType = CommandType.Text;
|
||||||
dbCommand.CommandTimeout = 0;
|
dbCommand.CommandTimeout = 0;
|
||||||
|
|
||||||
|
if (WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings.QueryExecutionSettings.IsAlwaysEncryptedParameterizationEnabled)
|
||||||
|
{
|
||||||
|
dbCommand.Parameterize();
|
||||||
|
}
|
||||||
|
|
||||||
List<DbColumn[]> columnSchemas = null;
|
List<DbColumn[]> columnSchemas = null;
|
||||||
if (getFullColumnSchema)
|
if (getFullColumnSchema)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
|
||||||
using Microsoft.SqlTools.Utility;
|
using Microsoft.SqlTools.Utility;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
||||||
{
|
{
|
||||||
@@ -168,6 +169,11 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool DefaultSqlCmdMode = false;
|
private bool DefaultSqlCmdMode = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default value for flag to enable Always Encrypted Parameterization
|
||||||
|
/// </summary>
|
||||||
|
private bool DefaultAlwaysEncryptedParameterizationValue = false;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Member Variables
|
#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
|
#endregion
|
||||||
|
|
||||||
#region Public Methods
|
#region Public Methods
|
||||||
@@ -691,6 +713,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
|||||||
AnsiWarnings = newSettings.AnsiWarnings;
|
AnsiWarnings = newSettings.AnsiWarnings;
|
||||||
AnsiNulls = newSettings.AnsiNulls;
|
AnsiNulls = newSettings.AnsiNulls;
|
||||||
IsSqlCmdMode = newSettings.IsSqlCmdMode;
|
IsSqlCmdMode = newSettings.IsSqlCmdMode;
|
||||||
|
IsAlwaysEncryptedParameterizationEnabled = newSettings.IsAlwaysEncryptedParameterizationEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ namespace Microsoft.SqlTools.ServiceLayer.SqlContext
|
|||||||
if (settings != null)
|
if (settings != null)
|
||||||
{
|
{
|
||||||
this.SqlTools.IntelliSense.Update(settings.SqlTools.IntelliSense);
|
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