From 5968e34371b7a4b34807f5bfbeb1c991ddadaa62 Mon Sep 17 00:00:00 2001 From: Mark Carrington <31017244+MarkMpn@users.noreply.github.com> Date: Thu, 8 Feb 2024 08:28:50 +0000 Subject: [PATCH] Avoid creating Linq expressions where possible for improved performance --- .../ExecutionPlan/ExpressionExtensions.cs | 633 +++++++++++------- 1 file changed, 382 insertions(+), 251 deletions(-) diff --git a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs index 84bf3570..d6ace9e7 100644 --- a/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs +++ b/MarkMpn.Sql4Cds.Engine/ExecutionPlan/ExpressionExtensions.cs @@ -22,18 +22,34 @@ static class ExpressionExtensions { class CompiledExpression { - public CompiledExpression(TSqlFragment expression, Func compiled) + public CompiledExpression(TSqlFragment expression, Expression converted, Func compiled) { Expression = expression; + Converted = converted; Compiled = compiled; } public TSqlFragment Expression { get; } + public Expression Converted { get; } public Func Compiled { get; } } + class IntermediateExpression + { + public IntermediateExpression(Expression converted, ParameterExpression[] parameters) + { + Converted = converted; + Parameters = parameters; + Lambda = Expression.Lambda(converted, parameters); + } + public Expression Converted { get; } + public ParameterExpression[] Parameters { get; } + public LambdaExpression Lambda { get; } + } + private static readonly TSqlParser _parser = new TSql160Parser(false); private static readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> _boolCache = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary _intermediateCache = new ConcurrentDictionary(); /// /// Gets the type of value that will be generated by an expression @@ -44,8 +60,14 @@ public CompiledExpression(TSqlFragment expression, FuncThe type of value that will be returned by the expression public static Type GetType(this TSqlFragment expr, ExpressionCompilationContext context, out DataTypeReference sqlType) { - var expression = ToExpression(expr, context, out _, out sqlType, out _); - return expression.Type; + ToExpression(expr, context, false, out _, out sqlType, out var cacheKey); + var details = _cache.GetOrAdd(cacheKey, __ => + { + var converted = ToExpression(expr, context, true, out var parameters, out _, out _); + var compiled = Expression.Lambda>(Expr.Box(converted), parameters).Compile(); + return new CompiledExpression(expr, converted, compiled); + }); + return details.Converted.Type; } /// @@ -56,10 +78,14 @@ public static Type GetType(this TSqlFragment expr, ExpressionCompilationContext /// A function that accepts an representing the context the expression is being evaluated in and returns the value of the expression public static Func Compile(this TSqlFragment expr, ExpressionCompilationContext context) { - var lambda = ToExpression(expr, context, out var parameters, out _, out var cacheKey); - var compiled = _cache.GetOrAdd(cacheKey, _ => new CompiledExpression(expr, Expression.Lambda>(Expr.Box(lambda), parameters).Compile())); - - return eec => compiled.Compiled(eec, expr); + ToExpression(expr, context, false, out _, out _, out var cacheKey); + var details = _cache.GetOrAdd(cacheKey, __ => + { + var converted = ToExpression(expr, context, true, out var parameters, out _, out _); + var compiled = Expression.Lambda>(Expr.Box(converted), parameters).Compile(); + return new CompiledExpression(expr, converted, compiled); + }); + return eec => details.Compiled(eec, expr); } /// @@ -70,79 +96,84 @@ public static Func Compile(this TSqlFragment /// A function that accepts aan representing the context the expression is being evaluated in and returns the value of the expression public static Func Compile(this BooleanExpression b, ExpressionCompilationContext context) { - var lambda = ToExpression(b, context, out var parameters, out _, out var cacheKey); - var compiled = _boolCache.GetOrAdd(cacheKey, _ => new CompiledExpression(b, Expression.Lambda>(Expression.IsTrue(lambda), parameters).Compile())); + ToExpression(b, context, false, out _, out _, out var cacheKey); + var details = _boolCache.GetOrAdd(cacheKey, __ => + { + var converted = ToExpression(b, context, true, out var parameters, out _, out _); + var compiled = Expression.Lambda>(Expression.IsTrue(converted), parameters).Compile(); + return new CompiledExpression(b, converted, compiled); + }); - return eec => compiled.Compiled(eec, b); + return eec => details.Compiled(eec, b); } - private static Expression ToExpression(this TSqlFragment expr, ExpressionCompilationContext context, out ParameterExpression[] parameters, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this TSqlFragment expr, ExpressionCompilationContext context, bool createExpression, out ParameterExpression[] parameters, out DataTypeReference sqlType, out string cacheKey) { - var contextParam = Expression.Parameter(typeof(ExpressionExecutionContext)); - var exprParam = Expression.Parameter(typeof(TSqlFragment)); + var contextParam = createExpression ? Expression.Parameter(typeof(ExpressionExecutionContext)) : null; + var exprParam = createExpression ? Expression.Parameter(typeof(TSqlFragment)) : null; Expression expression; if (expr is ColumnReferenceExpression col) - expression = ToExpression(col, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(col, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is IdentifierLiteral guid) - expression = ToExpression(guid, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(guid, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is IntegerLiteral i) - expression = ToExpression(i, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(i, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is MoneyLiteral money) - expression = ToExpression(money, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(money, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is NullLiteral n) - expression = ToExpression(n, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(n, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is NumericLiteral num) - expression = ToExpression(num, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(num, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is RealLiteral real) - expression = ToExpression(real, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(real, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is StringLiteral str) - expression = ToExpression(str, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(str, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is OdbcLiteral odbc) - expression = ToExpression(odbc, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(odbc, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is BooleanBinaryExpression boolBin) - expression = ToExpression(boolBin, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(boolBin, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is BooleanComparisonExpression cmp) - expression = ToExpression(cmp, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(cmp, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is BooleanParenthesisExpression boolParen) - expression = ToExpression(boolParen, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(boolParen, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is InPredicate inPred) - expression = ToExpression(inPred, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(inPred, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is BooleanIsNullExpression isNull) - expression = ToExpression(isNull, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(isNull, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is LikePredicate like) - expression = ToExpression(like, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(like, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is BooleanNotExpression not) - expression = ToExpression(not, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(not, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is FullTextPredicate fullText) - expression = ToExpression(fullText, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(fullText, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is Microsoft.SqlServer.TransactSql.ScriptDom.BinaryExpression bin) - expression = ToExpression(bin, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(bin, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is FunctionCall func) - expression = ToExpression(func, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(func, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is ParenthesisExpression paren) - expression = ToExpression(paren, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(paren, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is Microsoft.SqlServer.TransactSql.ScriptDom.UnaryExpression unary) - expression = ToExpression(unary, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(unary, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is VariableReference var) - expression = ToExpression(var, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(var, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is SimpleCaseExpression simpleCase) - expression = ToExpression(simpleCase, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(simpleCase, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is SearchedCaseExpression searchedCase) - expression = ToExpression(searchedCase, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(searchedCase, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is ConvertCall convert) - expression = ToExpression(convert, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(convert, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is CastCall cast) - expression = ToExpression(cast, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(cast, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is ParameterlessCall parameterless) - expression = ToExpression(parameterless, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(parameterless, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is GlobalVariableExpression global) - expression = ToExpression(global, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(global, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is ExpressionCallTarget callTarget) - expression = ToExpression(callTarget, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(callTarget, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else if (expr is DistinctPredicate distinct) - expression = ToExpression(distinct, context, contextParam, exprParam, out sqlType, out cacheKey); + expression = ToExpression(distinct, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); else throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 102, "Unhandled expression type", expr)); @@ -151,9 +182,11 @@ private static Expression ToExpression(this TSqlFragment expr, ExpressionCompila if (!Collation.TryParse(primary.Collation.Value, out var coll)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 447, $"Invalid collation '{primary.Collation}'", primary.Collation)); - if (expression.Type == typeof(SqlString) && sqlType is SqlDataTypeReferenceWithCollation sqlTypeWithCollation) + if (sqlType is SqlDataTypeReferenceWithCollation sqlTypeWithCollation) { - expression = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), expression, Expression.Constant(coll)); + if (createExpression) + expression = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), expression, Expression.Constant(coll)); + sqlType = new SqlDataTypeReferenceWithCollation { SqlDataTypeOption = sqlTypeWithCollation.SqlDataTypeOption, @@ -168,16 +201,24 @@ private static Expression ToExpression(this TSqlFragment expr, ExpressionCompila } } - parameters = new[] { contextParam, exprParam }; + parameters = createExpression ? new[] { contextParam, exprParam } : null; return expression; } - private static Expression InvokeSubExpression(this TParent parent, Func child, Expression> subExpressionSelector, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression InvokeSubExpression(this TParent parent, Func child, Expression> subExpressionSelector, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) where TParent : TSqlFragment where TChild : TSqlFragment { // Build an expression to invoke the child expression from within the context of the parent expression - var expr = child(parent).ToExpression(context, out var parameters, out sqlType, out cacheKey); + child(parent).ToExpression(context, false, out _, out sqlType, out cacheKey); + var details = _intermediateCache.GetOrAdd(cacheKey, __ => + { + var converted = child(parent).ToExpression(context, true, out var parameters, out _, out _); + return new IntermediateExpression(converted, parameters); + }); + + if (!createExpression || exprParam == null) + return details.Converted; if (!(subExpressionSelector is LambdaExpression lambda) || !(lambda.Body is MemberExpression memberAccessor) || @@ -186,22 +227,30 @@ private static Expression InvokeSubExpression(this TParent pare var subExpr = Expression.Property(Expression.Convert(exprParam, prop.DeclaringType), prop); - return Expression.Invoke(Expression.Lambda(expr, parameters), contextParam, subExpr); + return Expression.Invoke(details.Lambda, contextParam, subExpr); } - private static Expression InvokeSubExpression(this TParent parent, Func child, Expression> subExpressionSelector, int index, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression InvokeSubExpression(this TParent parent, Func child, Expression> subExpressionSelector, int index, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) where TParent : TSqlFragment where TChild : TSqlFragment { // Build an expression to invoke the child expression from within the context of the parent expression - var expr = child(parent).ToExpression(context, out var parameters, out sqlType, out cacheKey); + child(parent).ToExpression(context, false, out _, out sqlType, out cacheKey); + var details = _intermediateCache.GetOrAdd(cacheKey, __ => + { + var converted = child(parent).ToExpression(context, true, out var parameters, out _, out _); + return new IntermediateExpression(converted, parameters); + }); + + if (!createExpression || exprParam == null) + return details.Converted; var subExpr = Expression.Invoke(subExpressionSelector, Expression.Convert(exprParam, typeof(TParent)), Expression.Constant(index)); - return Expression.Invoke(Expression.Lambda(expr, parameters), contextParam, subExpr); + return Expression.Invoke(details.Lambda, contextParam, subExpr); } - private static Expression ToExpression(ColumnReferenceExpression col, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(ColumnReferenceExpression col, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { var name = col.GetColumnName(); @@ -244,6 +293,9 @@ private static Expression ToExpression(ColumnReferenceExpression col, Expression var returnType = sqlType.ToNetType(out _); cacheKey = $"({returnType})"; + if (!createExpression) + return null; + var entity = Expression.Property(contextParam, nameof(ExpressionExecutionContext.Entity)); var attr = Expr.Call( () => GetColumnName(Expr.Arg()), @@ -252,45 +304,57 @@ private static Expression ToExpression(ColumnReferenceExpression col, Expression return Expression.Convert(expr, returnType); } - private static Expression ToExpression(IdentifierLiteral guid, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(IdentifierLiteral guid, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.UniqueIdentifier; cacheKey = ""; + if (!createExpression) + return null; + return Expr.Call( () => SqlGuid.Parse(Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(IdentifierLiteral)), nameof(IdentifierLiteral.Value))); } - private static Expression ToExpression(IntegerLiteral i, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(IntegerLiteral i, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.Int; cacheKey = ""; + if (!createExpression) + return null; + return Expr.Call( () => SqlInt32.Parse(Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(IntegerLiteral)), nameof(IntegerLiteral.Value))); } - private static Expression ToExpression(MoneyLiteral money, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(MoneyLiteral money, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.Money; cacheKey = ""; + if (!createExpression) + return null; + return Expr.Call( () => SqlMoney.Parse(Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(MoneyLiteral)), nameof(MoneyLiteral.Value))); } - private static Expression ToExpression(NullLiteral n, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(NullLiteral n, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.ImplicitIntForNullLiteral; cacheKey = ""; + if (!createExpression) + return null; + return Expression.Constant(SqlInt32.Null); } - private static Expression ToExpression(NumericLiteral num, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(NumericLiteral num, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { // The type of the expression varies depending on the precision & scale implicit in the value // We need to parse the individual value now to determine the SQL type, but can still return a generic @@ -299,22 +363,28 @@ private static Expression ToExpression(NumericLiteral num, ExpressionCompilation sqlType = DataTypeHelpers.Decimal(value.Precision, value.Scale); cacheKey = $""; + if (!createExpression) + return null; + return Expr.Call( () => SqlDecimal.Parse(Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(NumericLiteral)), nameof(NumericLiteral.Value))); } - private static Expression ToExpression(RealLiteral real, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(RealLiteral real, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.Real; cacheKey = ""; + if (!createExpression) + return null; + return Expr.Call( () => SqlDouble.Parse(Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(RealLiteral)), nameof(RealLiteral.Value))); } - private static Expression ToExpression(StringLiteral str, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(StringLiteral str, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { // The type of the expression varies depending on whether the string is identified as unicode or not, and the length // We can still return a generic method that can be reused for all values as the actual .NET type is the same @@ -324,6 +394,9 @@ private static Expression ToExpression(StringLiteral str, ExpressionCompilationC cacheKey = ""; + if (!createExpression) + return null; + var value = Expression.Property(Expression.Convert(exprParam, typeof(StringLiteral)), nameof(StringLiteral.Value)); var expr = (Expression) Expression.Property(contextParam, nameof(ExpressionExecutionContext.PrimaryDataSource)); @@ -332,7 +405,7 @@ private static Expression ToExpression(StringLiteral str, ExpressionCompilationC return expr; } - private static Expression ToExpression(OdbcLiteral odbc, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(OdbcLiteral odbc, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { switch (odbc.OdbcLiteralType) { @@ -340,6 +413,9 @@ private static Expression ToExpression(OdbcLiteral odbc, ExpressionCompilationCo sqlType = DataTypeHelpers.Date; cacheKey = ""; + if (!createExpression) + return null; + var dateExpr = Expr.Call( () => DateTime.ParseExact(Expr.Arg(), Expr.Arg(), Expr.Arg(), Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(OdbcLiteral)), nameof(OdbcLiteral.Value)), @@ -353,6 +429,9 @@ private static Expression ToExpression(OdbcLiteral odbc, ExpressionCompilationCo sqlType = DataTypeHelpers.DateTime; cacheKey = ""; + if (!createExpression) + return null; + var dateTimeExpr = Expr.Call( () => DateTime.ParseExact(Expr.Arg(), Expr.Arg(), Expr.Arg(), Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(OdbcLiteral)), nameof(OdbcLiteral.Value)), @@ -366,6 +445,9 @@ private static Expression ToExpression(OdbcLiteral odbc, ExpressionCompilationCo sqlType = DataTypeHelpers.UniqueIdentifier; cacheKey = ""; + if (!createExpression) + return null; + return Expr.Call( () => SqlGuid.Parse(Expr.Arg()), Expression.Property(Expression.Convert(exprParam, typeof(OdbcLiteral)), nameof(OdbcLiteral.Value))); @@ -375,7 +457,7 @@ private static Expression ToExpression(OdbcLiteral odbc, ExpressionCompilationCo } } - private static Expression ToExpression(BooleanComparisonExpression cmp, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(BooleanComparisonExpression cmp, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { // Special case for field = func() where func is defined in FetchXmlConditionMethods if (cmp.FirstExpression is ColumnReferenceExpression && @@ -385,28 +467,28 @@ cmp.SecondExpression is FunctionCall func { var parameters = func.Parameters.Select((p, index) => { - var paramExpr = func.InvokeSubExpression(x => x.Parameters[index], (x, i) => x.Parameters[i], index, context, contextParam, exprParam, out var paramType, out var paramCacheKey); + var paramExpr = func.InvokeSubExpression(x => x.Parameters[index], (x, i) => x.Parameters[i], index, context, contextParam, exprParam, createExpression, out var paramType, out var paramCacheKey); return new { Expression = paramExpr, Type = paramType, CacheKey = paramCacheKey }; }).ToList(); - var colExpr = cmp.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, out var colType, out var colCacheKey); + var colExpr = cmp.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, createExpression, out var colType, out var colCacheKey); parameters.Insert(0, new { Expression = colExpr, Type = colType, CacheKey = colCacheKey }); var paramTypes = parameters.Select(p => p.Type).ToArray(); var paramCacheKeys = parameters.Select(p => p.CacheKey).ToArray(); var paramExpressions = parameters.Select(p => p.Expression).ToArray(); - var fetchXmlComparison = GetMethod(context, typeof(FetchXmlConditionMethods), context.PrimaryDataSource, func, paramTypes, paramCacheKeys, false, contextParam, ref paramExpressions, out sqlType, out var funcCacheKey); + var fetchXmlComparison = GetMethod(context, typeof(FetchXmlConditionMethods), context.PrimaryDataSource, func, paramTypes, paramCacheKeys, false, contextParam, createExpression, ref paramExpressions, out sqlType, out var funcCacheKey); if (fetchXmlComparison != null) { cacheKey = $"{colCacheKey} = FetchXml::{funcCacheKey}"; - return Expr.Call(fetchXmlComparison, paramExpressions); + return createExpression ? Expr.Call(fetchXmlComparison, paramExpressions) : null; } } sqlType = DataTypeHelpers.Bit; - var lhs = cmp.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, out var lhsType, out var lhsCacheKey); - var rhs = cmp.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, out var rhsType, out var rhsCacheKey); + var lhs = cmp.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, createExpression, out var lhsType, out var lhsCacheKey); + var rhs = cmp.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, createExpression, out var rhsType, out var rhsCacheKey); if (!SqlTypeConverter.CanMakeConsistentTypes(lhsType, rhsType, context.PrimaryDataSource, out var type)) { @@ -423,7 +505,7 @@ cmp.SecondExpression is FunctionCall func } if (!lhsType.IsSameAs(type)) - lhs = SqlTypeConverter.Convert(lhs, lhsType, type); + lhs = createExpression ? SqlTypeConverter.Convert(lhs, lhsType, type) : null; if (!rhsType.IsSameAs(type)) { @@ -443,7 +525,7 @@ cmp.SecondExpression is StringLiteral str && }; } - rhs = SqlTypeConverter.Convert(rhs, rhsType, type); + rhs = createExpression ? SqlTypeConverter.Convert(rhs, rhsType, type) : null; } AssertCollationSensitive(type, cmp.ComparisonType.ToString().ToLowerInvariant() + " operation", cmp); @@ -452,42 +534,42 @@ cmp.SecondExpression is StringLiteral str && { case BooleanComparisonType.Equals: cacheKey = lhsCacheKey + " = " + rhsCacheKey; - return Expression.Equal(lhs, rhs); + return createExpression ? Expression.Equal(lhs, rhs) : null; case BooleanComparisonType.GreaterThan: cacheKey = lhsCacheKey + " > " + rhsCacheKey; - return Expression.GreaterThan(lhs, rhs); + return createExpression ? Expression.GreaterThan(lhs, rhs) : null; case BooleanComparisonType.GreaterThanOrEqualTo: case BooleanComparisonType.NotLessThan: cacheKey = lhsCacheKey + " >= " + rhsCacheKey; - return Expression.GreaterThanOrEqual(lhs, rhs); + return createExpression ? Expression.GreaterThanOrEqual(lhs, rhs) : null; case BooleanComparisonType.LessThan: cacheKey = lhsCacheKey + " < " + rhsCacheKey; - return Expression.LessThan(lhs, rhs); + return createExpression ? Expression.LessThan(lhs, rhs) : null; case BooleanComparisonType.LessThanOrEqualTo: case BooleanComparisonType.NotGreaterThan: cacheKey = lhsCacheKey + " <= " + rhsCacheKey; - return Expression.LessThanOrEqual(lhs, rhs); + return createExpression ? Expression.LessThanOrEqual(lhs, rhs) : null; case BooleanComparisonType.NotEqualToBrackets: case BooleanComparisonType.NotEqualToExclamation: cacheKey = lhsCacheKey + " <> " + rhsCacheKey; - return Expression.NotEqual(lhs, rhs); + return createExpression ? Expression.NotEqual(lhs, rhs) : null; default: throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 102, "Unknown comparison type", cmp)); } } - private static Expression ToExpression(DistinctPredicate distinct, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(DistinctPredicate distinct, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.Bit; - var lhs = distinct.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, out var lhsType, out var lhsCacheKey); - var rhs = distinct.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, out var rhsType, out var rhsCacheKey); + var lhs = distinct.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, createExpression, out var lhsType, out var lhsCacheKey); + var rhs = distinct.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, createExpression, out var rhsType, out var rhsCacheKey); if (!SqlTypeConverter.CanMakeConsistentTypes(lhsType, rhsType, context.PrimaryDataSource, out var type)) { @@ -504,7 +586,7 @@ private static Expression ToExpression(DistinctPredicate distinct, ExpressionCom } if (!lhsType.IsSameAs(type)) - lhs = SqlTypeConverter.Convert(lhs, lhsType, type); + lhs = createExpression ? SqlTypeConverter.Convert(lhs, lhsType, type) : null; if (!rhsType.IsSameAs(type)) { @@ -524,13 +606,14 @@ distinct.SecondExpression is StringLiteral str && }; } - rhs = SqlTypeConverter.Convert(rhs, rhsType, type); + rhs = createExpression ? SqlTypeConverter.Convert(rhs, rhsType, type) : null; } AssertCollationSensitive(type, "IS DISTINCT FROM operation", distinct); // Using linked server decoding pseudocode from https://learn.microsoft.com/en-us/sql/t-sql/queries/is-distinct-from-transact-sql?view=sql-server-ver16#remarks - var expr = (Expression) Expression.AndAlso( + var expr = createExpression + ? (Expression) Expression.AndAlso( Expression.OrElse( Expression.OrElse( SqlTypeConverter.NullCheck(rhs), @@ -544,12 +627,13 @@ distinct.SecondExpression is StringLiteral str && SqlTypeConverter.NullCheck(rhs) ) ) - ); + ) + : null; if (distinct.IsNot) { cacheKey = lhsCacheKey + " IS NOT DISTINCT FROM " + rhsCacheKey; - expr = Expression.Not(expr); + expr = createExpression ? Expression.Not(expr) : null; } else { @@ -559,34 +643,34 @@ distinct.SecondExpression is StringLiteral str && return expr; } - private static Expression ToExpression(BooleanBinaryExpression bin, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(BooleanBinaryExpression bin, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { sqlType = DataTypeHelpers.Bit; - var lhs = bin.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, out _, out var lhsCacheKey); - var rhs = bin.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, out _, out var rhsCacheKey); + var lhs = bin.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, createExpression, out _, out var lhsCacheKey); + var rhs = bin.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, createExpression, out _, out var rhsCacheKey); if (bin.BinaryExpressionType == BooleanBinaryExpressionType.And) { cacheKey = lhsCacheKey + " AND " + rhsCacheKey; - return Expression.AndAlso(lhs, rhs); + return createExpression ? Expression.AndAlso(lhs, rhs) : null; } cacheKey = lhsCacheKey + " OR " + rhsCacheKey; - return Expression.OrElse(lhs, rhs); + return createExpression ? Expression.OrElse(lhs, rhs) : null; } - private static Expression ToExpression(BooleanParenthesisExpression paren, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(BooleanParenthesisExpression paren, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var expr = paren.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out sqlType, out cacheKey); + var expr = paren.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); cacheKey = "(" + cacheKey + ")"; return expr; } - private static Expression ToExpression(Microsoft.SqlServer.TransactSql.ScriptDom.BinaryExpression bin, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(Microsoft.SqlServer.TransactSql.ScriptDom.BinaryExpression bin, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var lhs = bin.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, out var lhsSqlType, out var lhsCacheKey); - var rhs = bin.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, out var rhsSqlType, out var rhsCacheKey); + var lhs = bin.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, true, out var lhsSqlType, out var lhsCacheKey); + var rhs = bin.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, true, out var rhsSqlType, out var rhsCacheKey); if (!SqlTypeConverter.CanMakeConsistentTypes(lhsSqlType, rhsSqlType, context.PrimaryDataSource, out var type)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {lhsSqlType.ToSql()} is incompatible with {rhsSqlType.ToSql()}", bin)); @@ -677,8 +761,6 @@ private static Expression ToExpression(Microsoft.SqlServer.TransactSql.ScriptDom // Special case for SqlString length & collation calculation if (lhsSqlType is SqlDataTypeReferenceWithCollation lhsSql && rhsSqlType is SqlDataTypeReferenceWithCollation rhsSql && - lhs.Type == typeof(SqlString) && - rhs.Type == typeof(SqlString) && lhsSql.Parameters.Count == 1 && rhsSql.Parameters.Count == 1) { @@ -781,7 +863,7 @@ private static SqlDateTime SubtractSqlDateTime(SqlDateTime lhs, SqlDateTime rhs) return lhs - ts; } - private static MethodInfo GetMethod(FunctionCall func, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out Expression[] paramExpressions, out DataTypeReference sqlType, out string cacheKey) + private static MethodInfo GetMethod(FunctionCall func, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out Expression[] paramExpressions, out DataTypeReference sqlType, out string cacheKey) { var paramExpressionsWithType = func.Parameters .Select((param, index) => @@ -807,7 +889,7 @@ private static MethodInfo GetMethod(FunctionCall func, ExpressionCompilationCont throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 155, $"'{param.ToSql()}' is not a recognized datepart option", param)); } - return new { Expression = (Expression)Expression.Constant(col.MultiPartIdentifier.Identifiers.Single().Value), Type = (DataTypeReference)DataTypeHelpers.NVarChar(col.MultiPartIdentifier.Identifiers.Single().Value.Length, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault), CacheKey = col.MultiPartIdentifier.Identifiers.Single().Value }; + return new { Expression = createExpression ? (Expression)Expression.Constant(col.MultiPartIdentifier.Identifiers.Single().Value) : null, Type = (DataTypeReference)DataTypeHelpers.NVarChar(col.MultiPartIdentifier.Identifiers.Single().Value.Length, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault), CacheKey = col.MultiPartIdentifier.Identifiers.Single().Value }; } // Special case for ISJSON - second optional parameter looks like a field but is actually a JSON data type @@ -829,10 +911,10 @@ private static MethodInfo GetMethod(FunctionCall func, ExpressionCompilationCont throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 155, $"'{param.ToSql()}' is not a recognized isjson option", param)); } - return new { Expression = (Expression)Expression.Constant(col.MultiPartIdentifier.Identifiers.Single().Value), Type = (DataTypeReference)DataTypeHelpers.NVarChar(col.MultiPartIdentifier.Identifiers.Single().Value.Length, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault), CacheKey = col.MultiPartIdentifier.Identifiers.Single().Value }; + return new { Expression = createExpression ? (Expression)Expression.Constant(col.MultiPartIdentifier.Identifiers.Single().Value) : null, Type = (DataTypeReference)DataTypeHelpers.NVarChar(col.MultiPartIdentifier.Identifiers.Single().Value.Length, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault), CacheKey = col.MultiPartIdentifier.Identifiers.Single().Value }; } - var paramExpr = func.InvokeSubExpression(x => x.Parameters[index], (x, i) => x.Parameters[i], index, context, contextParam, exprParam, out var paramType, out var paramCacheKey); + var paramExpr = func.InvokeSubExpression(x => x.Parameters[index], (x, i) => x.Parameters[i], index, context, contextParam, exprParam, createExpression, out var paramType, out var paramCacheKey); return new { Expression = paramExpr, Type = paramType, CacheKey = paramCacheKey }; }) .ToList(); @@ -840,7 +922,7 @@ private static MethodInfo GetMethod(FunctionCall func, ExpressionCompilationCont if (func.CallTarget != null) { // If this function has a target (e.g. xml functions), add the target as the first parameter - var targetParam = func.InvokeSubExpression(x => x.CallTarget, x => x.CallTarget, context, contextParam, exprParam, out var targetType, out var targetCacheKey); + var targetParam = func.InvokeSubExpression(x => x.CallTarget, x => x.CallTarget, context, contextParam, exprParam, createExpression, out var targetType, out var targetCacheKey); paramExpressionsWithType.Insert(0, new { Expression = targetParam, Type = targetType, CacheKey = targetCacheKey }); } @@ -848,10 +930,10 @@ private static MethodInfo GetMethod(FunctionCall func, ExpressionCompilationCont .Select(kvp => kvp.Expression) .ToArray(); - return GetMethod(context, typeof(ExpressionFunctions), context.PrimaryDataSource, func, paramExpressionsWithType.Select(kvp => kvp.Type).ToArray(), paramExpressionsWithType.Select(kvp => kvp.CacheKey).ToArray(), true, contextParam, ref paramExpressions, out sqlType, out cacheKey); + return GetMethod(context, typeof(ExpressionFunctions), context.PrimaryDataSource, func, paramExpressionsWithType.Select(kvp => kvp.Type).ToArray(), paramExpressionsWithType.Select(kvp => kvp.CacheKey).ToArray(), true, contextParam, createExpression, ref paramExpressions, out sqlType, out cacheKey); } - private static MethodInfo GetMethod(ExpressionCompilationContext context, Type targetType, DataSource primaryDataSource, FunctionCall func, DataTypeReference[] paramTypes, string[] paramCacheKeys, bool throwOnMissing, ParameterExpression contextParam, ref Expression[] paramExpressions, out DataTypeReference sqlType, out string cacheKey) + private static MethodInfo GetMethod(ExpressionCompilationContext context, Type targetType, DataSource primaryDataSource, FunctionCall func, DataTypeReference[] paramTypes, string[] paramCacheKeys, bool throwOnMissing, ParameterExpression contextParam, bool createExpression, ref Expression[] paramExpressions, out DataTypeReference sqlType, out string cacheKey) { // Find a method that implements this function var methods = targetType @@ -971,28 +1053,37 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t if (paramType == typeof(ExpressionExecutionContext)) { - var paramsWithOptions = new List(paramExpressions); - paramsWithOptions.Insert(i, contextParam); - paramExpressions = paramsWithOptions.ToArray(); + if (createExpression) + { + var paramsWithOptions = new List(paramExpressions); + paramsWithOptions.Insert(i, contextParam); + paramExpressions = paramsWithOptions.ToArray(); + } hiddenParams++; continue; } if (paramType == typeof(INodeSchema)) { - var paramsWithOptions = new List(paramExpressions); - paramsWithOptions.Insert(i, Expression.Constant(context.Schema)); - paramExpressions = paramsWithOptions.ToArray(); + if (createExpression) + { + var paramsWithOptions = new List(paramExpressions); + paramsWithOptions.Insert(i, Expression.Constant(context.Schema)); + paramExpressions = paramsWithOptions.ToArray(); + } hiddenParams++; continue; } if (i >= paramTypes.Length && parameters[i].GetCustomAttribute() != null) { - var paramsWithDefaultValue = new Expression[paramExpressions.Length + 1]; - paramExpressions.CopyTo(paramsWithDefaultValue, 0); - paramsWithDefaultValue[i] = Expression.Constant(SqlTypeConverter.GetNullValue(paramType)); - paramExpressions = paramsWithDefaultValue; + if (createExpression) + { + var paramsWithDefaultValue = new Expression[paramExpressions.Length + 1]; + paramExpressions.CopyTo(paramsWithDefaultValue, 0); + paramsWithDefaultValue[i] = Expression.Constant(SqlTypeConverter.GetNullValue(paramType)); + paramExpressions = paramsWithDefaultValue; + } hiddenParams++; continue; } @@ -1002,10 +1093,13 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t if (parameters[i].GetCustomAttribute() != null) { cacheKey += $"(TYPE:{sourceType.ToSql()})"; - var paramsWithType = new Expression[paramExpressions.Length + 1]; - paramExpressions.CopyTo(paramsWithType, 0); - paramsWithType[i] = Expression.Constant(sourceType); - paramExpressions = paramsWithType; + if (createExpression) + { + var paramsWithType = new Expression[paramExpressions.Length + 1]; + paramExpressions.CopyTo(paramsWithType, 0); + paramsWithType[i] = Expression.Constant(sourceType); + paramExpressions = paramsWithType; + } hiddenParams++; } else @@ -1018,7 +1112,9 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 9500, $"The data type '{typeLiteral.Value}' used in the {func.FunctionName.Value} method is invalid", typeLiteral)); cacheKey +=$"(TYPE:{parsedType.ToSql()})"; - paramExpressions[i] = Expression.Constant(parsedType); + + if (createExpression) + paramExpressions[i] = Expression.Constant(parsedType); if (parameters[i].GetCustomAttribute() != null) sqlType = parsedType; @@ -1034,7 +1130,10 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t throw new NotSupportedQueryFragmentException($"The argument {i} of the XML data type method \"{func.FunctionName.Value}\" must be a string literal"); cacheKey += $"(XPATH:{xpathLiteral.Value})"; - paramExpressions[i] = Expression.Constant(XPath2Expression.Compile(xpathLiteral.Value, XPath2ExpressionContext.XmlNamespaceManager)); + + if (createExpression) + paramExpressions[i] = Expression.Constant(XPath2Expression.Compile(xpathLiteral.Value, XPath2ExpressionContext.XmlNamespaceManager)); + continue; } @@ -1050,7 +1149,7 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {paramTypes[i].ToSql()} is incompatible with {paramType.ToSqlType(primaryDataSource).ToSql()}", i < paramOffset ? func : func.Parameters[i - paramOffset])); } - if (parameters.Length > 0 && parameters.Last().ParameterType.IsArray) + if (createExpression && parameters.Length > 0 && parameters.Last().ParameterType.IsArray) { var arrayType = parameters.Last().ParameterType.GetElementType(); var arrayMembers = new List(); @@ -1119,7 +1218,9 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t if (!collationParam.Collation.Equals(collation.Collation)) { // TODO: Ensure same collation isn't reused when cached compiled expression is executed for different collations - paramExpressions[i] = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), paramExpressions[i], Expression.Constant(collation.Collation)); + if (createExpression) + paramExpressions[i] = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), paramExpressions[i], Expression.Constant(collation.Collation)); + paramTypes[i] = new SqlDataTypeReferenceWithCollation { SqlDataTypeOption = collationParam.SqlDataTypeOption, @@ -1142,7 +1243,7 @@ private static MethodInfo GetMethod(ExpressionCompilationContext context, Type t return method; } - private static Expression ToExpression(this FunctionCall func, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this FunctionCall func, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { if (func.OverClause != null) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 40517, "Window functions are not supported", func)); @@ -1155,7 +1256,7 @@ private static Expression ToExpression(this FunctionCall func, ExpressionCompila // change so we can return it without any further processing if (func.FunctionName.Value == "ExplicitCollation" && func.Parameters.Count == 1) { - var converted = func.InvokeSubExpression(x => x.Parameters[0], (x, i) => x.Parameters[i], 0, context, contextParam, exprParam, out sqlType, out cacheKey); + var converted = func.InvokeSubExpression(x => x.Parameters[0], (x, i) => x.Parameters[i], 0, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); if (!(sqlType is SqlDataTypeReferenceWithCollation coll) || !coll.SqlDataTypeOption.IsStringType() || @@ -1170,7 +1271,10 @@ private static Expression ToExpression(this FunctionCall func, ExpressionCompila } // Find the method to call and get the expressions for the parameter values - var method = GetMethod(func, context, contextParam, exprParam, out var paramValues, out sqlType, out cacheKey); + var method = GetMethod(func, context, contextParam, exprParam, createExpression, out var paramValues, out sqlType, out cacheKey); + + if (!createExpression) + return null; // Convert the parameters to the expected types var parameters = method.GetParameters(); @@ -1189,47 +1293,49 @@ private static Expression ToExpression(this FunctionCall func, ExpressionCompila return expr; } - private static Expression ToExpression(this ParenthesisExpression paren, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this ParenthesisExpression paren, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var expr = paren.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out sqlType, out cacheKey); + var expr = paren.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); cacheKey = "(" + cacheKey + ")"; return expr; } - private static Expression ToExpression(this ExpressionCallTarget callTarget, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this ExpressionCallTarget callTarget, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - return callTarget.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out sqlType, out cacheKey); + var expr = callTarget.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); + cacheKey += ""; + return expr; } - private static Expression ToExpression(this Microsoft.SqlServer.TransactSql.ScriptDom.UnaryExpression unary, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this Microsoft.SqlServer.TransactSql.ScriptDom.UnaryExpression unary, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var value = unary.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out sqlType, out cacheKey); + var value = unary.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); switch (unary.UnaryExpressionType) { case UnaryExpressionType.Positive: cacheKey = "+" + cacheKey; - return Expression.UnaryPlus(value); + return createExpression ? Expression.UnaryPlus(value) : null; case UnaryExpressionType.Negative: cacheKey = "-" + cacheKey; - return Expression.Negate(value); + return createExpression ? Expression.Negate(value) : null; case UnaryExpressionType.BitwiseNot: cacheKey = "~" + cacheKey; - return Expression.Not(value); + return createExpression ? Expression.Not(value) : null; default: throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 102, "Unknown unary operator", unary)); } } - private static Expression ToExpression(this InPredicate inPred, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this InPredicate inPred, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { if (inPred.Subquery != null) throw new NotSupportedQueryFragmentException("Subquery should have been eliminated by query plan", inPred); - var exprValue = inPred.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out var exprType, out cacheKey); + var exprValue = inPred.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out var exprType, out cacheKey); if (inPred.NotDefined) cacheKey += " NOT IN ("; else @@ -1239,25 +1345,28 @@ private static Expression ToExpression(this InPredicate inPred, ExpressionCompil for (var i = 0; i < inPred.Values.Count; i++) { - var comparisonValue = inPred.InvokeSubExpression(x => x.Values[i], (x, j) => x.Values[j], i, context, contextParam, exprParam, out var comparisonType, out var comparisonCacheKey); + var comparisonValue = inPred.InvokeSubExpression(x => x.Values[i], (x, j) => x.Values[j], i, context, contextParam, exprParam, createExpression, out var comparisonType, out var comparisonCacheKey); if (!SqlTypeConverter.CanMakeConsistentTypes(exprType, comparisonType, context.PrimaryDataSource, out var type)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {exprType.ToSql()} is incompatible with {comparisonType.ToSql()}", inPred)); - var convertedExprValue = exprValue; + if (createExpression) + { + var convertedExprValue = exprValue; - if (!exprType.IsSameAs(type)) - convertedExprValue = SqlTypeConverter.Convert(convertedExprValue, exprType, type); + if (!exprType.IsSameAs(type)) + convertedExprValue = SqlTypeConverter.Convert(convertedExprValue, exprType, type); - if (!comparisonType.IsSameAs(type)) - comparisonValue = SqlTypeConverter.Convert(comparisonValue, comparisonType, type); + if (!comparisonType.IsSameAs(type)) + comparisonValue = SqlTypeConverter.Convert(comparisonValue, comparisonType, type); - var comparison = inPred.NotDefined ? Expression.NotEqual(convertedExprValue, comparisonValue) : Expression.Equal(convertedExprValue, comparisonValue); + var comparison = inPred.NotDefined ? Expression.NotEqual(convertedExprValue, comparisonValue) : Expression.Equal(convertedExprValue, comparisonValue); - if (result == null) - result = comparison; - else - result = inPred.NotDefined ? Expression.AndAlso(result, comparison) : Expression.OrElse(result, comparison); + if (result == null) + result = comparison; + else + result = inPred.NotDefined ? Expression.AndAlso(result, comparison) : Expression.OrElse(result, comparison); + } if (i > 0) cacheKey += ", "; @@ -1270,42 +1379,48 @@ private static Expression ToExpression(this InPredicate inPred, ExpressionCompil return result; } - private static Expression ToExpression(this VariableReference var, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this VariableReference var, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { if (context.ParameterTypes == null || !context.ParameterTypes.TryGetValue(var.Name, out sqlType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 137, $"Must declare the scalar variable \"{var.Name}\"", var)); + var netType = sqlType.ToNetType(out _); + cacheKey = $"({netType})"; + + if (!createExpression) + return null; + var parameters = Expression.Property(contextParam, nameof(ExpressionExecutionContext.ParameterValues)); var varName = Expression.Property(Expression.Convert(exprParam, typeof(VariableReference)), nameof(VariableReference.Name)); var expr = Expression.Property(parameters, typeof(IDictionary).GetCustomAttribute().MemberName, varName); - var netType = sqlType.ToNetType(out _); - - cacheKey = $"({netType})"; return Expression.Convert(expr, netType); } - private static Expression ToExpression(this GlobalVariableExpression var, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this GlobalVariableExpression var, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { if (context.ParameterTypes == null || !context.ParameterTypes.TryGetValue(var.Name, out sqlType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(15, 137, $"Must declare the scalar variable \"{var.Name}\"", var)); + var netType = sqlType.ToNetType(out _); + cacheKey = $"({netType})"; + + if (!createExpression) + return null; + var parameters = Expression.Property(contextParam, nameof(ExpressionExecutionContext.ParameterValues)); var varName = Expression.Property(Expression.Convert(exprParam, typeof(GlobalVariableExpression)), nameof(GlobalVariableExpression.Name)); var expr = Expression.Property(parameters, typeof(IDictionary).GetCustomAttribute().MemberName, varName); - var netType = sqlType.ToNetType(out _); - - cacheKey = $"({netType})"; return Expression.Convert(expr, netType); } - private static Expression ToExpression(this BooleanIsNullExpression isNull, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this BooleanIsNullExpression isNull, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var value = isNull.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out _, out cacheKey); - value = SqlTypeConverter.NullCheck(value); + var value = isNull.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out _, out cacheKey); + value = createExpression ? SqlTypeConverter.NullCheck(value) : null; if (isNull.IsNot) { - value = Expression.Not(value); + value = createExpression ? Expression.Not(value) : null; cacheKey += " IS NOT NULL"; } else @@ -1313,47 +1428,47 @@ private static Expression ToExpression(this BooleanIsNullExpression isNull, Expr cacheKey += " IS NULL"; } - value = SqlTypeConverter.Convert(value, typeof(SqlBoolean)); + value = createExpression ? SqlTypeConverter.Convert(value, typeof(SqlBoolean)) : null; sqlType = DataTypeHelpers.Bit; return value; } - private static Expression ToExpression(this LikePredicate like, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this LikePredicate like, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { DataTypeReference escapeType = null; string escapeCacheKey = null; - var value = like.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, out var valueType, out var valueCacheKey); - var pattern = like.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, out var patternType, out var patternCacheKey); - var escape = like.EscapeExpression == null ? null : like.InvokeSubExpression(x => x.EscapeExpression, x => x.EscapeExpression, context, contextParam, exprParam, out escapeType, out escapeCacheKey); + var value = like.InvokeSubExpression(x => x.FirstExpression, x => x.FirstExpression, context, contextParam, exprParam, createExpression, out var valueType, out var valueCacheKey); + var pattern = like.InvokeSubExpression(x => x.SecondExpression, x => x.SecondExpression, context, contextParam, exprParam, createExpression, out var patternType, out var patternCacheKey); + var escape = like.EscapeExpression == null ? null : like.InvokeSubExpression(x => x.EscapeExpression, x => x.EscapeExpression, context, contextParam, exprParam, createExpression, out escapeType, out escapeCacheKey); sqlType = DataTypeHelpers.Bit; var stringType = DataTypeHelpers.NVarChar(Int32.MaxValue, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault); - if (value.Type != typeof(SqlString)) + if (valueType.GetDataTypeFamily() != DataTypeFamily.Character) { if (!SqlTypeConverter.CanChangeTypeImplicit(valueType, stringType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {valueType.ToSql()} is incompatible with {stringType.ToSql()}", like.FirstExpression)); - value = SqlTypeConverter.Convert(value, valueType, stringType); + value = createExpression ? SqlTypeConverter.Convert(value, valueType, stringType) : null; valueType = stringType; } - if (pattern.Type != typeof(SqlString)) + if (patternType.GetDataTypeFamily() != DataTypeFamily.Character) { if (!SqlTypeConverter.CanChangeTypeImplicit(patternType, stringType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {patternType.ToSql()} is incompatible with {stringType.ToSql()}", like.FirstExpression)); - pattern = SqlTypeConverter.Convert(pattern, patternType, stringType); + pattern = createExpression ? SqlTypeConverter.Convert(pattern, patternType, stringType) : null; patternType = stringType; } - if (escape != null && escape.Type != typeof(SqlString)) + if (escapeType != null && escapeType.GetDataTypeFamily() != DataTypeFamily.Character) { if (!SqlTypeConverter.CanChangeTypeImplicit(escapeType, stringType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {escapeType.ToSql()} is incompatible with {stringType.ToSql()}", like.FirstExpression)); - escape = SqlTypeConverter.Convert(escape, escapeType, stringType); + escape = createExpression ? SqlTypeConverter.Convert(escape, escapeType, stringType) : null; escapeType = stringType; } @@ -1375,17 +1490,20 @@ private static Expression ToExpression(this LikePredicate like, ExpressionCompil AssertCollationSensitive(stringType, "like operation", like); - if (escape == null) + if (escape == null && createExpression) escape = Expression.Constant(SqlString.Null); - if (pattern.NodeType == ExpressionType.Constant && (escape == null || escape.NodeType == ExpressionType.Constant)) + if (like.SecondExpression is StringLiteral patternLiteral && (like.EscapeExpression == null || like.EscapeExpression is StringLiteral)) { // Do a one-off conversion to regex try { - var regex = LikeToRegex(SqlTypeConverter.ConvertCollation((SqlString)((ConstantExpression)pattern).Value, collation), (SqlString)(((ConstantExpression)escape)?.Value ?? SqlString.Null), false); - cacheKey = $"{valueCacheKey} REGEX {regex}"; - return Expr.Call(() => Like(Expr.Arg(), Expr.Arg(), Expr.Arg()), value, Expression.Constant(regex), Expression.Constant(like.NotDefined)); + var regex = LikeToRegex(SqlTypeConverter.ConvertCollation(patternLiteral.Value, collation), like.EscapeExpression == null ? SqlString.Null : ((StringLiteral)like.EscapeExpression).Value, false); + if (like.NotDefined) + cacheKey = $"{valueCacheKey} NOT REGEX {regex}"; + else + cacheKey = $"{valueCacheKey} REGEX {regex}"; + return createExpression ? Expr.Call(() => Like(Expr.Arg(), Expr.Arg(), Expr.Arg()), value, Expression.Constant(regex), Expression.Constant(like.NotDefined)) : null; } catch (ArgumentException ex) { @@ -1393,14 +1511,21 @@ private static Expression ToExpression(this LikePredicate like, ExpressionCompil } } - value = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), value, Expression.Constant(collation)); - pattern = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), pattern, Expression.Constant(collation)); + if (createExpression) + { + value = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), value, Expression.Constant(collation)); + pattern = Expr.Call(() => SqlTypeConverter.ConvertCollation(Expr.Arg(), Expr.Arg()), pattern, Expression.Constant(collation)); + } + + if (like.NotDefined) + cacheKey = $"{valueCacheKey} NOT LIKE {patternCacheKey}"; + else + cacheKey = $"{valueCacheKey} LIKE {patternCacheKey}"; - cacheKey = $"{valueCacheKey} LIKE {patternCacheKey}"; if (escapeCacheKey != null) cacheKey += $" ESCAPE {escapeCacheKey}"; - return Expr.Call(() => Like(Expr.Arg(), Expr.Arg(), Expr.Arg(), Expr.Arg()), value, pattern, escape, Expression.Constant(like.NotDefined)); + return createExpression ? Expr.Call(() => Like(Expr.Arg(), Expr.Arg(), Expr.Arg(), Expr.Arg()), value, pattern, escape, Expression.Constant(like.NotDefined)) : null; } internal static Regex LikeToRegex(SqlString pattern, SqlString escape, bool patIndex) @@ -1559,24 +1684,24 @@ internal static string RemoveDiacritics(string text) .Normalize(NormalizationForm.FormC); } - private static Expression ToExpression(this SimpleCaseExpression simpleCase, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this SimpleCaseExpression simpleCase, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { // Convert all the different elements to expressions - var value = simpleCase.InvokeSubExpression(x => x.InputExpression, x => x.InputExpression, context, contextParam, exprParam, out var valueType, out var valueCacheKey); + var value = simpleCase.InvokeSubExpression(x => x.InputExpression, x => x.InputExpression, context, contextParam, exprParam, createExpression, out var valueType, out var valueCacheKey); var whenClauses = simpleCase.WhenClauses.Select((_, index) => { - var whenExpr = simpleCase.InvokeSubExpression(x => x.WhenClauses[index].WhenExpression, (x, i) => x.WhenClauses[i].WhenExpression, index, context, contextParam, exprParam, out var whenType, out var whenCacheKey); + var whenExpr = simpleCase.InvokeSubExpression(x => x.WhenClauses[index].WhenExpression, (x, i) => x.WhenClauses[i].WhenExpression, index, context, contextParam, exprParam, createExpression, out var whenType, out var whenCacheKey); return new { Expression = whenExpr, Type = whenType, CacheKey = whenCacheKey }; }).ToList(); var caseTypes = new DataTypeReference[whenClauses.Count]; var thenClauses = simpleCase.WhenClauses.Select((_, index) => { - var thenExpr = simpleCase.InvokeSubExpression(x => x.WhenClauses[index].ThenExpression, (x, i) => x.WhenClauses[i].ThenExpression, index, context, contextParam, exprParam, out var thenType, out var thenCacheKey); + var thenExpr = simpleCase.InvokeSubExpression(x => x.WhenClauses[index].ThenExpression, (x, i) => x.WhenClauses[i].ThenExpression, index, context, contextParam, exprParam, createExpression, out var thenType, out var thenCacheKey); return new { Expression = thenExpr, Type = thenType, CacheKey = thenCacheKey }; }).ToList(); DataTypeReference elseType = null; string elseCacheKey = null; - var elseValue = simpleCase.ElseExpression == null ? null : simpleCase.InvokeSubExpression(x => x.ElseExpression, x => x.ElseExpression, context, contextParam, exprParam, out elseType, out elseCacheKey); + var elseValue = simpleCase.ElseExpression == null ? null : simpleCase.InvokeSubExpression(x => x.ElseExpression, x => x.ElseExpression, context, contextParam, exprParam, createExpression, out elseType, out elseCacheKey); // First pass to determine final return type DataTypeReference type = null; @@ -1610,39 +1735,42 @@ private static Expression ToExpression(this SimpleCaseExpression simpleCase, Exp // add the earlier cases Expression result = null; - if (elseValue != null) + if (createExpression) { - if (!elseType.IsSameAs(type)) - elseValue = SqlTypeConverter.Convert(elseValue, elseType, type); + if (elseValue != null) + { + if (!elseType.IsSameAs(type)) + elseValue = SqlTypeConverter.Convert(elseValue, elseType, type); - result = elseValue; - } - else - { - result = Expression.Constant(SqlTypeConverter.GetNullValue(type.ToNetType(out _))); - } + result = elseValue; + } + else + { + result = Expression.Constant(SqlTypeConverter.GetNullValue(type.ToNetType(out _))); + } - for (var i = simpleCase.WhenClauses.Count - 1; i >= 0; i--) - { - var valueCopy = value; - var whenValue = whenClauses[i].Expression; - var whenType = whenClauses[i].Type; - var caseType = caseTypes[i]; + for (var i = simpleCase.WhenClauses.Count - 1; i >= 0; i--) + { + var valueCopy = value; + var whenValue = whenClauses[i].Expression; + var whenType = whenClauses[i].Type; + var caseType = caseTypes[i]; - if (!valueType.IsSameAs(caseType)) - valueCopy = SqlTypeConverter.Convert(valueCopy, valueType, caseType); + if (!valueType.IsSameAs(caseType)) + valueCopy = SqlTypeConverter.Convert(valueCopy, valueType, caseType); - if (!whenType.IsSameAs(caseType)) - whenValue = SqlTypeConverter.Convert(whenValue, whenType, caseType); + if (!whenType.IsSameAs(caseType)) + whenValue = SqlTypeConverter.Convert(whenValue, whenType, caseType); - var comparison = Expression.Equal(valueCopy, whenValue); - var returnValue = thenClauses[i].Expression; - var returnType = thenClauses[i].Type; + var comparison = Expression.Equal(valueCopy, whenValue); + var returnValue = thenClauses[i].Expression; + var returnType = thenClauses[i].Type; - if (!returnType.IsSameAs(type)) - returnValue = SqlTypeConverter.Convert(returnValue, returnType, type); + if (!returnType.IsSameAs(type)) + returnValue = SqlTypeConverter.Convert(returnValue, returnType, type); - result = Expression.Condition(Expression.IsTrue(comparison), returnValue, result); + result = Expression.Condition(Expression.IsTrue(comparison), returnValue, result); + } } cacheKey = "CASE " + valueCacheKey; @@ -1656,22 +1784,22 @@ private static Expression ToExpression(this SimpleCaseExpression simpleCase, Exp return result; } - private static Expression ToExpression(this SearchedCaseExpression searchedCase, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this SearchedCaseExpression searchedCase, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { // Convert all the different elements to expressions var whenClauses = searchedCase.WhenClauses.Select((_, index) => { - var whenExpr = searchedCase.InvokeSubExpression(x => x.WhenClauses[index].WhenExpression, (x, i) => x.WhenClauses[i].WhenExpression, index, context, contextParam, exprParam, out var whenType, out var whenCacheKey); + var whenExpr = searchedCase.InvokeSubExpression(x => x.WhenClauses[index].WhenExpression, (x, i) => x.WhenClauses[i].WhenExpression, index, context, contextParam, exprParam, createExpression, out var whenType, out var whenCacheKey); return new { Expression = whenExpr, Type = whenType, CacheKey = whenCacheKey }; }).ToList(); var thenClauses = searchedCase.WhenClauses.Select((_, index) => { - var thenExpr = searchedCase.InvokeSubExpression(x => x.WhenClauses[index].ThenExpression, (x, i) => x.WhenClauses[i].ThenExpression, index, context, contextParam, exprParam, out var thenType, out var thenCacheKey ); + var thenExpr = searchedCase.InvokeSubExpression(x => x.WhenClauses[index].ThenExpression, (x, i) => x.WhenClauses[i].ThenExpression, index, context, contextParam, exprParam, createExpression, out var thenType, out var thenCacheKey ); return new { Expression = thenExpr, Type = thenType, CacheKey = thenCacheKey }; }).ToList(); DataTypeReference elseType = null; string elseCacheKey = null; - var elseValue = searchedCase.ElseExpression == null ? null : searchedCase.InvokeSubExpression(x => x.ElseExpression, x => x.ElseExpression, context, contextParam, exprParam, out elseType, out elseCacheKey); + var elseValue = searchedCase.ElseExpression == null ? null : searchedCase.InvokeSubExpression(x => x.ElseExpression, x => x.ElseExpression, context, contextParam, exprParam, createExpression, out elseType, out elseCacheKey); // First pass to determine final return type DataTypeReference type = null; @@ -1698,33 +1826,36 @@ private static Expression ToExpression(this SearchedCaseExpression searchedCase, // add the earlier cases Expression result = null; - if (elseValue != null) + if (createExpression) { - if (!elseType.IsSameAs(type)) - elseValue = SqlTypeConverter.Convert(elseValue, elseType, type); + if (elseValue != null) + { + if (!elseType.IsSameAs(type)) + elseValue = SqlTypeConverter.Convert(elseValue, elseType, type); - result = elseValue; - } - else - { - result = Expression.Constant(SqlTypeConverter.GetNullValue(type.ToNetType(out _))); - } + result = elseValue; + } + else + { + result = Expression.Constant(SqlTypeConverter.GetNullValue(type.ToNetType(out _))); + } - var bitType = DataTypeHelpers.Bit; + var bitType = DataTypeHelpers.Bit; - for (var i = whenClauses.Count - 1; i >= 0; i--) - { - var whenValue = whenClauses[i].Expression; - var whenType = whenClauses[i].Type; - var returnValue = thenClauses[i].Expression; - var returnType = thenClauses[i].Type; + for (var i = whenClauses.Count - 1; i >= 0; i--) + { + var whenValue = whenClauses[i].Expression; + var whenType = whenClauses[i].Type; + var returnValue = thenClauses[i].Expression; + var returnType = thenClauses[i].Type; - whenValue = SqlTypeConverter.Convert(whenValue, whenType, bitType); - whenValue = Expression.IsTrue(whenValue); + whenValue = SqlTypeConverter.Convert(whenValue, whenType, bitType); + whenValue = Expression.IsTrue(whenValue); - returnValue = SqlTypeConverter.Convert(returnValue, returnType, type); + returnValue = SqlTypeConverter.Convert(returnValue, returnType, type); - result = Expression.Condition(whenValue, returnValue, result); + result = Expression.Condition(whenValue, returnValue, result); + } } cacheKey = "CASE"; @@ -1738,11 +1869,11 @@ private static Expression ToExpression(this SearchedCaseExpression searchedCase, return result; } - private static Expression ToExpression(this BooleanNotExpression not, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this BooleanNotExpression not, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var value = not.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, out sqlType, out cacheKey); + var value = not.InvokeSubExpression(x => x.Expression, x => x.Expression, context, contextParam, exprParam, createExpression, out sqlType, out cacheKey); cacheKey = "NOT " + cacheKey; - return Expression.Not(value); + return createExpression ? Expression.Not(value) : null; } private static readonly Dictionary _typeMapping = new Dictionary @@ -1860,12 +1991,12 @@ public static DataTypeReference ToSqlType(this Type type, DataSource dataSource) return _netTypeMapping[type]; } - private static Expression ToExpression(this ConvertCall convert, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this ConvertCall convert, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var value = convert.InvokeSubExpression(x => x.Parameter, x => x.Parameter, context, contextParam, exprParam, out var valueType, out var valueCacheKey); + var value = convert.InvokeSubExpression(x => x.Parameter, x => x.Parameter, context, contextParam, exprParam, createExpression, out var valueType, out var valueCacheKey); DataTypeReference styleType = null; string styleCacheKey = null; - var style = convert.Style == null ? null : convert.InvokeSubExpression(x => x.Style, x => x.Style, context, contextParam, exprParam, out styleType, out styleCacheKey); + var style = convert.Style == null ? null : convert.InvokeSubExpression(x => x.Style, x => x.Style, context, contextParam, exprParam, createExpression, out styleType, out styleCacheKey); sqlType = convert.DataType; @@ -1901,12 +2032,12 @@ private static Expression Convert(ExpressionCompilationContext context, Expressi cacheKey += ", " + styleCacheKey; cacheKey += ")"; - return SqlTypeConverter.Convert(value, valueType, sqlType, style, styleType, expr); + return value == null ? null : SqlTypeConverter.Convert(value, valueType, sqlType, style, styleType, expr); } - private static Expression ToExpression(this CastCall cast, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this CastCall cast, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { - var value = cast.InvokeSubExpression(x => x.Parameter, x => x.Parameter, context, contextParam, exprParam, out var valueType, out var valueCacheKey); + var value = cast.InvokeSubExpression(x => x.Parameter, x => x.Parameter, context, contextParam, exprParam, createExpression, out var valueType, out var valueCacheKey); sqlType = cast.DataType; return Convert(context, value, valueType, valueCacheKey, sqlType, null, null, null, cast, out cacheKey); @@ -1914,7 +2045,7 @@ private static Expression ToExpression(this CastCall cast, ExpressionCompilation private static readonly Regex _containsParser = new Regex("^\\S+( OR \\S+)*$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static Expression ToExpression(this FullTextPredicate fullText, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this FullTextPredicate fullText, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { // Only support simple CONTAINS calls to handle multi-select optionsets for now if (fullText.FullTextFunctionType != FullTextFunctionType.Contains) @@ -1932,13 +2063,13 @@ private static Expression ToExpression(this FullTextPredicate fullText, Expressi if (fullText.LanguageTerm != null) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 40517, "LANGUAGE is not currently supported", fullText.LanguageTerm)); - var col = fullText.InvokeSubExpression(x => x.Columns[0], (x, i) => x.Columns[i], 0, context, contextParam, exprParam, out var colType, out var colCacheKey); + var col = fullText.InvokeSubExpression(x => x.Columns[0], (x, i) => x.Columns[i], 0, context, contextParam, exprParam, createExpression, out var colType, out var colCacheKey); var stringType = DataTypeHelpers.NVarChar(Int32.MaxValue, context.PrimaryDataSource.DefaultCollation, CollationLabel.CoercibleDefault); if (!SqlTypeConverter.CanChangeTypeImplicit(colType, stringType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 7670, $"Column '{fullText.Columns[0].ToSql()}' cannot be used for full-text search because it is not a character-based, XML, image, JSON or varbinary(max) type column or it is encrypted", fullText.Columns[0])); - col = SqlTypeConverter.Convert(col, colType, stringType); + col = createExpression ? SqlTypeConverter.Convert(col, colType, stringType) : null; sqlType = DataTypeHelpers.Bit; if (fullText.Value is StringLiteral lit) @@ -1948,18 +2079,18 @@ private static Expression ToExpression(this FullTextPredicate fullText, Expressi var words = GetContainsWords(lit.Value, true); cacheKey = $"{colCacheKey} REGEX CONTAINS ({String.Join(", ", words.Select(w => w.ToString()))})"; - return Expr.Call(() => Contains(Expr.Arg(), Expr.Arg()), col, Expression.Constant(words)); + return createExpression ? Expr.Call(() => Contains(Expr.Arg(), Expr.Arg()), col, Expression.Constant(words)) : null; } - var value = fullText.InvokeSubExpression(x => x.Value, x => x.Value, context, contextParam, exprParam, out var valueType, out var valueCacheKey); + var value = fullText.InvokeSubExpression(x => x.Value, x => x.Value, context, contextParam, exprParam, createExpression, out var valueType, out var valueCacheKey); if (!SqlTypeConverter.CanChangeTypeImplicit(valueType, stringType)) throw new NotSupportedQueryFragmentException(new Sql4CdsError(16, 206, $"Operand type clash: {valueType.ToSql()} is incompatible with {stringType.ToSql()}", fullText.Value)); - value = SqlTypeConverter.Convert(value, valueType, stringType); + value = createExpression ? SqlTypeConverter.Convert(value, valueType, stringType) : null; cacheKey = $"{colCacheKey} CONTAINS {valueCacheKey}"; - return Expr.Call(() => Contains(Expr.Arg(), Expr.Arg()), col, value); + return createExpression ? Expr.Call(() => Contains(Expr.Arg(), Expr.Arg()), col, value) : null; } private static SqlBoolean Contains(SqlString col, SqlString value) @@ -1995,19 +2126,19 @@ private static Regex[] GetContainsWords(string pattern, bool compile) .ToArray(); } - private static Expression ToExpression(this ParameterlessCall parameterless, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, out DataTypeReference sqlType, out string cacheKey) + private static Expression ToExpression(this ParameterlessCall parameterless, ExpressionCompilationContext context, ParameterExpression contextParam, ParameterExpression exprParam, bool createExpression, out DataTypeReference sqlType, out string cacheKey) { switch (parameterless.ParameterlessCallType) { case ParameterlessCallType.CurrentTimestamp: sqlType = DataTypeHelpers.DateTime; cacheKey = "CURRENT_TIMESTAMP"; - return Expr.Call(() => GetCurrentTimestamp(Expr.Arg()), contextParam); + return createExpression ? Expr.Call(() => GetCurrentTimestamp(Expr.Arg()), contextParam) : null; default: sqlType = DataTypeHelpers.EntityReference; cacheKey = "CURRENT_USER"; - return Expr.Call(() => GetCurrentUser(Expr.Arg()), contextParam); + return createExpression ? Expr.Call(() => GetCurrentUser(Expr.Arg()), contextParam) : null; } }