| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| using System; |
| using System.Text; |
| |
| namespace Apache.Ignite.Linq.Impl |
| { |
| using System.Collections.Generic; |
| using System.Diagnostics; |
| using System.Diagnostics.CodeAnalysis; |
| using System.Linq; |
| using System.Linq.Expressions; |
| using System.Reflection; |
| using Apache.Ignite.Core.Cache; |
| using Apache.Ignite.Core.Cache.Configuration; |
| using Apache.Ignite.Core.Impl.Cache; |
| using Apache.Ignite.Core.Impl.Common; |
| using Remotion.Linq; |
| using Remotion.Linq.Clauses; |
| using Remotion.Linq.Clauses.Expressions; |
| using Remotion.Linq.Clauses.ResultOperators; |
| using Remotion.Linq.Parsing; |
| |
| /// <summary> |
| /// Expression visitor, transforms query subexpressions (such as Where clauses) to SQL. |
| /// </summary> |
| internal class CacheQueryExpressionVisitor : ThrowingExpressionVisitor |
| { |
| /** */ |
| private readonly bool _useStar; |
| |
| /** */ |
| private readonly CacheQueryModelVisitor _modelVisitor; |
| |
| /** */ |
| private static readonly CopyOnWriteConcurrentDictionary<MemberInfo, string> FieldNameMap = |
| new CopyOnWriteConcurrentDictionary<MemberInfo, string>(); |
| |
| /** */ |
| private readonly bool _includeAllFields; |
| |
| /** */ |
| private readonly bool _visitEntireSubQueryModel; |
| |
| /// <summary> |
| /// Initializes a new instance of the <see cref="CacheQueryExpressionVisitor" /> class. |
| /// </summary> |
| /// <param name="modelVisitor">The _model visitor.</param> |
| /// <param name="useStar"> |
| /// Flag indicating that star '*' qualifier should be used |
| /// for the whole-table select instead of _key, _val. |
| /// </param> |
| /// <param name="includeAllFields"> |
| /// Flag indicating that star '*' qualifier should be used |
| /// for the whole-table select as well as _key, _val. |
| /// </param> |
| /// <param name="visitEntireSubQueryModel"> |
| /// Flag indicating that subquery |
| /// should be visited as full query |
| /// </param> |
| public CacheQueryExpressionVisitor(CacheQueryModelVisitor modelVisitor, bool useStar, bool includeAllFields, |
| bool visitEntireSubQueryModel) |
| { |
| Debug.Assert(modelVisitor != null); |
| |
| _modelVisitor = modelVisitor; |
| _useStar = useStar; |
| _includeAllFields = includeAllFields; |
| _visitEntireSubQueryModel = visitEntireSubQueryModel; |
| } |
| |
| /// <summary> |
| /// Gets the result builder. |
| /// </summary> |
| public StringBuilder ResultBuilder |
| { |
| get { return _modelVisitor.Builder; } |
| } |
| |
| /// <summary> |
| /// Gets the parameters. |
| /// </summary> |
| public IList<object> Parameters |
| { |
| get { return _modelVisitor.Parameters; } |
| } |
| |
| /// <summary> |
| /// Gets the aliases. |
| /// </summary> |
| private AliasDictionary Aliases |
| { |
| get { return _modelVisitor.Aliases; } |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0")] |
| protected override Expression VisitUnary(UnaryExpression expression) |
| { |
| var closeBracket = false; |
| |
| switch (expression.NodeType) |
| { |
| case ExpressionType.Negate: |
| ResultBuilder.Append("(-"); |
| closeBracket = true; |
| break; |
| |
| case ExpressionType.Not: |
| ResultBuilder.Append("(not "); |
| closeBracket = true; |
| break; |
| |
| case ExpressionType.Convert: |
| // Ignore, let the db do the conversion |
| break; |
| |
| default: |
| return base.VisitUnary(expression); |
| } |
| |
| Visit(expression.Operand); |
| |
| if(closeBracket) |
| ResultBuilder.Append(')'); |
| |
| return expression; |
| } |
| |
| /// <summary> |
| /// Visits the binary function. |
| /// </summary> |
| /// <param name="expression">The expression.</param> |
| /// <returns>True if function detected, otherwise false.</returns> |
| private bool VisitBinaryFunc(BinaryExpression expression) |
| { |
| if (expression.NodeType == ExpressionType.Add && expression.Left.Type == typeof (string)) |
| ResultBuilder.Append("concat("); |
| else if (expression.NodeType == ExpressionType.Coalesce) |
| ResultBuilder.Append("coalesce("); |
| else |
| return false; |
| |
| Visit(expression.Left); |
| ResultBuilder.Append(", "); |
| Visit(expression.Right); |
| ResultBuilder.Append(')'); |
| |
| return true; |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitBinary(BinaryExpression expression) |
| { |
| // Either func or operator |
| if (VisitBinaryFunc(expression)) |
| return expression; |
| |
| ResultBuilder.Append('('); |
| |
| Visit(expression.Left); |
| |
| switch (expression.NodeType) |
| { |
| case ExpressionType.Equal: |
| { |
| // Use `IS [NOT] DISTINCT FROM` for correct null comparison semantics. |
| // E.g. when user says `.Where(x => x == null)`, it should work, but with `=` it does not. |
| ResultBuilder.Append(" IS NOT DISTINCT FROM "); |
| |
| break; |
| } |
| |
| case ExpressionType.NotEqual: |
| { |
| ResultBuilder.Append(" IS DISTINCT FROM "); |
| |
| break; |
| } |
| |
| case ExpressionType.AndAlso: |
| case ExpressionType.And: |
| ResultBuilder.Append(" and "); |
| break; |
| |
| case ExpressionType.OrElse: |
| case ExpressionType.Or: |
| ResultBuilder.Append(" or "); |
| break; |
| |
| case ExpressionType.Add: |
| ResultBuilder.Append(" + "); |
| break; |
| |
| case ExpressionType.Subtract: |
| ResultBuilder.Append(" - "); |
| break; |
| |
| case ExpressionType.Multiply: |
| ResultBuilder.Append(" * "); |
| break; |
| |
| case ExpressionType.Modulo: |
| ResultBuilder.Append(" % "); |
| break; |
| |
| case ExpressionType.Divide: |
| ResultBuilder.Append(" / "); |
| break; |
| |
| case ExpressionType.GreaterThan: |
| ResultBuilder.Append(" > "); |
| break; |
| |
| case ExpressionType.GreaterThanOrEqual: |
| ResultBuilder.Append(" >= "); |
| break; |
| |
| case ExpressionType.LessThan: |
| ResultBuilder.Append(" < "); |
| break; |
| |
| case ExpressionType.LessThanOrEqual: |
| ResultBuilder.Append(" <= "); |
| break; |
| |
| case ExpressionType.Coalesce: |
| break; |
| |
| default: |
| base.VisitBinary(expression); |
| break; |
| } |
| |
| Visit(expression.Right); |
| ResultBuilder.Append(')'); |
| |
| return expression; |
| } |
| |
| /** <inheritdoc /> */ |
| public override Expression Visit(Expression expression) |
| { |
| var paramExpr = expression as ParameterExpression; |
| |
| if (paramExpr != null) |
| { |
| // This happens only with compiled queries, where parameters come from enclosing lambda. |
| AppendParameter(paramExpr); |
| return expression; |
| } |
| |
| return base.Visit(expression); |
| } |
| |
| /** <inheritdoc /> */ |
| protected override Expression VisitQuerySourceReference(QuerySourceReferenceExpression expression) |
| { |
| // In some cases of Join clause different handling should be introduced |
| var joinClause = expression.ReferencedQuerySource as JoinClause; |
| if (joinClause != null && ExpressionWalker.GetCacheQueryable(expression, false) == null) |
| { |
| var tableName = Aliases.GetTableAlias(expression); |
| var fieldname = Aliases.GetFieldAlias(expression); |
| |
| ResultBuilder.AppendFormat("{0}.{1}", tableName, fieldname); |
| } |
| else if (joinClause != null && joinClause.InnerSequence is SubQueryExpression) |
| { |
| var subQueryExpression = (SubQueryExpression) joinClause.InnerSequence; |
| base.Visit(subQueryExpression.QueryModel.SelectClause.Selector); |
| } |
| else |
| { |
| // Count, sum, max, min expect a single field or * |
| // In other cases we need both parts of cache entry |
| var format = _includeAllFields |
| ? "{0}.*, {0}._KEY, {0}._VAL" |
| : _useStar |
| ? "{0}.*" |
| : "{0}._KEY, {0}._VAL"; |
| |
| var tableName = Aliases.GetTableAlias(expression); |
| |
| ResultBuilder.AppendFormat(format, tableName); |
| } |
| |
| return expression; |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitMember(MemberExpression expression) |
| { |
| // Field hierarchy is flattened (Person.Address.Street is just Street), append as is, do not call Visit. |
| |
| // Property call (string.Length, DateTime.Month, etc). |
| if (MethodVisitor.VisitPropertyCall(expression, this)) |
| return expression; |
| |
| // Special case: grouping |
| if (VisitGroupByMember(expression.Expression)) |
| return expression; |
| |
| var queryable = ExpressionWalker.GetCacheQueryable(expression, false); |
| |
| if (queryable != null) |
| { |
| // Find where the projection comes from. |
| expression = ExpressionWalker.GetProjectedMember(expression.Expression, expression.Member) ?? |
| expression; |
| |
| var fieldName = GetEscapedFieldName(expression, queryable); |
| |
| ResultBuilder.AppendFormat("{0}.{1}", Aliases.GetTableAlias(expression), fieldName); |
| } |
| else |
| AppendParameter(ExpressionWalker.EvaluateExpression<object>(expression)); |
| |
| return expression; |
| } |
| |
| /// <summary> |
| /// Gets the name of the field from a member expression, with quotes when necessary. |
| /// </summary> |
| private static string GetEscapedFieldName(MemberExpression expression, ICacheQueryableInternal queryable) |
| { |
| var sqlEscapeAll = queryable.CacheConfiguration.SqlEscapeAll; |
| var fieldName = GetFieldName(expression, queryable); |
| |
| return sqlEscapeAll ? string.Format("\"{0}\"", fieldName) : fieldName; |
| } |
| |
| /// <summary> |
| /// Gets the name of the field from a member expression, with quotes when necessary. |
| /// </summary> |
| private static string GetFieldName(MemberExpression expression, ICacheQueryableInternal queryable, |
| bool ignoreAlias = false) |
| { |
| var fieldName = GetMemberFieldName(expression.Member); |
| |
| // Look for a field alias |
| var cacheCfg = queryable.CacheConfiguration; |
| |
| if (cacheCfg.QueryEntities == null || cacheCfg.QueryEntities.All(x => x.Aliases == null)) |
| { |
| // There are no aliases defined - early exit. |
| return fieldName; |
| } |
| |
| // Find query entity by key-val types |
| var entity = GetQueryEntity(queryable, cacheCfg); |
| |
| if (entity == null) |
| { |
| return fieldName; |
| } |
| |
| // There are some aliases for the current query type |
| // Calculate full field name and look for alias |
| var fullFieldName = fieldName; |
| var member = expression; |
| |
| while ((member = member.Expression as MemberExpression) != null && |
| member.Member.DeclaringType != queryable.ElementType) |
| { |
| fullFieldName = GetFieldName(member, queryable, true) + "." + fullFieldName; |
| } |
| |
| var alias = ignoreAlias ? null : ((IQueryEntityInternal)entity).GetAlias(fullFieldName); |
| |
| return alias ?? fieldName; |
| } |
| |
| /// <summary> |
| /// Finds matching query entity in the cache configuration. |
| /// </summary> |
| private static QueryEntity GetQueryEntity(ICacheQueryableInternal queryable, CacheConfiguration cacheCfg) |
| { |
| if (cacheCfg.QueryEntities.Count == 1) |
| { |
| return cacheCfg.QueryEntities.Single(); |
| } |
| |
| var keyValTypes = queryable.ElementType.GetGenericArguments(); |
| |
| Debug.Assert(keyValTypes.Length == 2); |
| |
| // PERF: No LINQ. |
| foreach (var e in cacheCfg.QueryEntities) |
| { |
| if (e.Aliases != null |
| && (e.KeyType == keyValTypes[0] || e.KeyTypeName == keyValTypes[0].FullName) |
| && (e.ValueType == keyValTypes[1] || e.ValueTypeName == keyValTypes[1].FullName)) |
| { |
| return e; |
| } |
| } |
| |
| return null; |
| } |
| |
| /// <summary> |
| /// Gets the name of the member field. |
| /// </summary> |
| [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", |
| Justification = "Not applicable.")] |
| private static string GetMemberFieldName(MemberInfo member) |
| { |
| string fieldName; |
| |
| if (FieldNameMap.TryGetValue(member, out fieldName)) |
| return fieldName; |
| |
| return FieldNameMap.GetOrAdd(member, m => |
| { |
| // Special case: _key, _val |
| if (m.DeclaringType != null && |
| m.DeclaringType.IsGenericType && |
| m.DeclaringType.GetGenericTypeDefinition() == typeof (ICacheEntry<,>)) |
| return "_" + m.Name.ToUpperInvariant().Substring(0, 3); |
| |
| var qryFieldAttr = m.GetCustomAttributes(true) |
| .OfType<QuerySqlFieldAttribute>().FirstOrDefault(); |
| |
| return qryFieldAttr == null || string.IsNullOrEmpty(qryFieldAttr.Name) |
| ? m.Name |
| : qryFieldAttr.Name; |
| }); |
| } |
| |
| /// <summary> |
| /// Visits the group by member. |
| /// </summary> |
| private bool VisitGroupByMember(Expression expression) |
| { |
| var srcRef = expression as QuerySourceReferenceExpression; |
| if (srcRef == null) |
| return false; |
| |
| var from = srcRef.ReferencedQuerySource as IFromClause; |
| if (from == null) |
| return false; |
| |
| var subQry = from.FromExpression as SubQueryExpression; |
| if (subQry == null) |
| return false; |
| |
| var grpBy = subQry.QueryModel.ResultOperators.OfType<GroupResultOperator>().FirstOrDefault(); |
| if (grpBy == null) |
| return false; |
| |
| Visit(grpBy.KeySelector); |
| |
| return true; |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitConstant(ConstantExpression expression) |
| { |
| if (MethodVisitor.VisitConstantCall(expression, this)) |
| { |
| return expression; |
| } |
| |
| AppendParameter(expression.Value); |
| |
| return expression; |
| } |
| |
| /// <summary> |
| /// Appends the parameter. |
| /// </summary> |
| public void AppendParameter(object value) |
| { |
| ResultBuilder.Append('?'); |
| |
| _modelVisitor.Parameters.Add(value); |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitMethodCall(MethodCallExpression expression) |
| { |
| MethodVisitor.VisitMethodCall(expression, this); |
| |
| return expression; |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitNew(NewExpression expression) |
| { |
| VisitArguments(expression.Arguments); |
| |
| return expression; |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitInvocation(InvocationExpression expression) |
| { |
| throw new NotSupportedException("The LINQ expression '" + expression + |
| "' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable() or ToList()."); |
| } |
| |
| /** <inheritdoc /> */ |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitConditional(ConditionalExpression expression) |
| { |
| ResultBuilder.Append("casewhen("); |
| |
| Visit(expression.Test); |
| |
| // Explicit type specification is required when all arguments of CASEWHEN are parameters |
| ResultBuilder.Append(", cast("); |
| Visit(expression.IfTrue); |
| ResultBuilder.AppendFormat(" as {0}), ", SqlTypes.GetSqlTypeName(expression.Type) ?? "other"); |
| |
| Visit(expression.IfFalse); |
| ResultBuilder.Append(')'); |
| |
| return expression; |
| } |
| |
| /** <inheritdoc /> */ |
| |
| [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")] |
| protected override Expression VisitSubQuery(SubQueryExpression expression) |
| { |
| var subQueryModel = expression.QueryModel; |
| |
| var contains = subQueryModel.ResultOperators.FirstOrDefault() as ContainsResultOperator; |
| |
| // Check if IEnumerable.Contains is used. |
| if (subQueryModel.ResultOperators.Count == 1 && contains != null) |
| { |
| VisitContains(subQueryModel, contains); |
| } |
| else if (_visitEntireSubQueryModel) |
| { |
| ResultBuilder.Append('('); |
| _modelVisitor.VisitQueryModel(subQueryModel, false, true); |
| ResultBuilder.Append(')'); |
| } |
| else |
| { |
| // This happens when New expression uses a subquery, in a GroupBy. |
| _modelVisitor.VisitSelectors(expression.QueryModel, false); |
| } |
| |
| return expression; |
| } |
| |
| /// <summary> |
| /// Visits IEnumerable.Contains |
| /// </summary> |
| private void VisitContains(QueryModel subQueryModel, ContainsResultOperator contains) |
| { |
| ResultBuilder.Append('('); |
| |
| var fromExpression = subQueryModel.MainFromClause.FromExpression; |
| |
| var queryable = ExpressionWalker.GetCacheQueryable(fromExpression, false); |
| |
| if (queryable != null) |
| { |
| Visit(contains.Item); |
| |
| ResultBuilder.Append(" IN ("); |
| if (_visitEntireSubQueryModel) |
| { |
| _modelVisitor.VisitQueryModel(subQueryModel, false, true); |
| } |
| else |
| { |
| _modelVisitor.VisitQueryModel(subQueryModel); |
| } |
| |
| ResultBuilder.Append(')'); |
| } |
| else |
| { |
| var inValues = ExpressionWalker.EvaluateEnumerableValues(fromExpression).ToArray(); |
| |
| var hasNulls = inValues.Any(o => o == null); |
| |
| if (hasNulls) |
| { |
| ResultBuilder.Append('('); |
| } |
| |
| Visit(contains.Item); |
| |
| ResultBuilder.Append(" IN ("); |
| AppendInParameters(inValues); |
| ResultBuilder.Append(')'); |
| |
| if (hasNulls) |
| { |
| ResultBuilder.Append(") OR "); |
| Visit(contains.Item); |
| ResultBuilder.Append(" IS NULL"); |
| } |
| } |
| |
| ResultBuilder.Append(')'); |
| } |
| |
| /// <summary> |
| /// Appends not null parameters using ", " as delimeter. |
| /// </summary> |
| private void AppendInParameters(IEnumerable<object> values) |
| { |
| var first = true; |
| |
| foreach (var val in values) |
| { |
| if (val == null) |
| continue; |
| |
| if (!first) |
| { |
| ResultBuilder.Append(", "); |
| } |
| |
| first = false; |
| |
| AppendParameter(val); |
| } |
| } |
| |
| /** <inheritdoc /> */ |
| protected override Exception CreateUnhandledItemException<T>(T unhandledItem, string visitMethod) |
| { |
| return new NotSupportedException(string.Format("The expression '{0}' (type: {1}) is not supported.", |
| unhandledItem, typeof (T))); |
| } |
| |
| /// <summary> |
| /// Visits multiple arguments. |
| /// </summary> |
| /// <param name="arguments">The arguments.</param> |
| private void VisitArguments(IEnumerable<Expression> arguments) |
| { |
| var first = true; |
| |
| foreach (var e in arguments) |
| { |
| if (!first) |
| { |
| if (_useStar) |
| throw new NotSupportedException("Aggregate functions do not support multiple fields"); |
| |
| ResultBuilder.Append(", "); |
| } |
| |
| first = false; |
| |
| Visit(e); |
| } |
| } |
| } |
| } |