modules/platforms/dotnet/Apache.Ignite/Internal/Linq/ExpressionWalker.cs (212 lines of code) (raw):

/* * 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. */ namespace Apache.Ignite.Internal.Linq; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; /// <summary> /// Walks expression trees to extract query and table name info. /// </summary> internal static class ExpressionWalker { /// <summary> /// Gets the queryable. /// </summary> /// <param name="fromClause">FROM clause.</param> /// <param name="throwWhenNotFound">Whether to throw when not found or return null.</param> /// <returns>Ignite internal queryable.</returns> public static IIgniteQueryableInternal? GetIgniteQueryable(IFromClause fromClause, bool throwWhenNotFound = true) { return GetIgniteQueryable(fromClause.FromExpression, throwWhenNotFound); } /// <summary> /// Gets the queryable. /// </summary> /// <param name="joinClause">JOIN clause.</param> /// <param name="throwWhenNotFound">Whether to throw when not found or return null.</param> /// <returns>Ignite internal queryable.</returns> public static IIgniteQueryableInternal? GetIgniteQueryable(JoinClause joinClause, bool throwWhenNotFound = true) { return GetIgniteQueryable(joinClause.InnerSequence, throwWhenNotFound); } /// <summary> /// Gets the queryable. /// </summary> /// <param name="expression">Expression.</param> /// <param name="throwWhenNotFound">Whether to throw when not found or return null.</param> /// <returns>Ignite internal queryable.</returns> public static IIgniteQueryableInternal? GetIgniteQueryable(Expression expression, bool throwWhenNotFound = true) { if (expression is SubQueryExpression subQueryExp) { return GetIgniteQueryable(subQueryExp.QueryModel.MainFromClause, throwWhenNotFound); } if (expression is QuerySourceReferenceExpression srcRefExp) { if (srcRefExp.ReferencedQuerySource is IFromClause fromSource) { return GetIgniteQueryable(fromSource, throwWhenNotFound); } if (srcRefExp.ReferencedQuerySource is JoinClause joinSource) { return GetIgniteQueryable(joinSource, throwWhenNotFound); } throw new NotSupportedException("Unexpected query source: " + srcRefExp.ReferencedQuerySource); } if (expression is MemberExpression memberExpr) { if (memberExpr.Type.IsGenericType && memberExpr.Type.GetGenericTypeDefinition() == typeof(IQueryable<>)) { return EvaluateExpression<IIgniteQueryableInternal>(memberExpr); } return GetIgniteQueryable(memberExpr.Expression!, throwWhenNotFound); } if (expression is ConstantExpression { Value: IIgniteQueryableInternal queryable }) { return queryable; } if (expression is MethodCallExpression callExpr) { // This is usually a nested query with a call to AsCacheQueryable(). return (IIgniteQueryableInternal) Expression.Lambda(callExpr).Compile().DynamicInvoke()!; } if (throwWhenNotFound) { throw new NotSupportedException("Unexpected query source: " + expression); } return null; } /// <summary> /// Gets the projected member. /// Queries can have multiple projections, e.g. <c>qry.Select(person => new {Foo = person.Name})</c>. /// This method finds the original member expression from the given projected expression, e.g finds /// <c>Person.Name</c> from <c>Foo</c>. /// </summary> /// <param name="expression">Expression.</param> /// <param name="memberHint">Member info.</param> /// <returns>Projected member.</returns> public static MemberExpression? GetProjectedMember(Expression expression, MemberInfo memberHint) { if (expression is SubQueryExpression subQueryExp) { var selector = subQueryExp.QueryModel.SelectClause.Selector; if (selector is NewExpression { Members: { } } newExpr) { Debug.Assert(newExpr.Members.Count == newExpr.Arguments.Count, "newExpr.Members.Count == newExpr.Arguments.Count"); for (var i = 0; i < newExpr.Members.Count; i++) { var member = newExpr.Members[i]; if (member == memberHint) { return newExpr.Arguments[i] as MemberExpression; } } } if (selector is MemberInitExpression initExpr) { foreach (var binding in initExpr.Bindings) { if (binding.Member == memberHint && binding.BindingType == MemberBindingType.Assignment) { return ((MemberAssignment)binding).Expression as MemberExpression; } } } return GetProjectedMember(subQueryExp.QueryModel.MainFromClause.FromExpression, memberHint); } if (expression is QuerySourceReferenceExpression { ReferencedQuerySource: IFromClause fromSource }) { return GetProjectedMember(fromSource.FromExpression, memberHint); } return null; } /// <summary> /// Gets the original QuerySourceReferenceExpression. /// </summary> /// <param name="expression">Expression.</param> /// <param name="throwWhenNotFound">Whether to throw when not found or return null.</param> /// <returns>Query source reference.</returns> public static QuerySourceReferenceExpression? GetQuerySourceReference( Expression expression, bool throwWhenNotFound = true) { if (expression is QuerySourceReferenceExpression reference) { return reference; } if (expression is UnaryExpression unary) { return GetQuerySourceReference(unary.Operand, false); } if (expression is BinaryExpression binary) { return GetQuerySourceReference(binary.Left, false) ?? GetQuerySourceReference(binary.Right, false); } if (throwWhenNotFound) { throw new NotSupportedException("Unexpected query source: " + expression); } return null; } /// <summary> /// Gets the query source. /// </summary> /// <param name="expression">Expression.</param> /// <param name="memberHint">Member expression.</param> /// <returns>Query source.</returns> public static IQuerySource? GetQuerySource(Expression expression, MemberExpression? memberHint = null) { if (memberHint != null) { if (expression is NewExpression { Members: { } } newExpr) { for (var i = 0; i < newExpr.Members.Count; i++) { var member = newExpr.Members[i]; if (member == memberHint.Member) { return GetQuerySource(newExpr.Arguments[i]); } } } } if (expression is SubQueryExpression subQueryExp) { var source = GetQuerySource(subQueryExp.QueryModel.SelectClause.Selector, memberHint); if (source != null) { return source; } return subQueryExp.QueryModel.MainFromClause; } if (expression is QuerySourceReferenceExpression srcRefExp) { if (srcRefExp.ReferencedQuerySource is IFromClause fromSource) { var source = GetQuerySource(fromSource.FromExpression, memberHint); if (source != null) { return source; } return fromSource; } if (srcRefExp.ReferencedQuerySource is JoinClause joinSource) { return GetQuerySource(joinSource.InnerSequence, memberHint) ?? joinSource; } throw new NotSupportedException("Unexpected query source: " + srcRefExp.ReferencedQuerySource); } if (expression is MemberExpression { Expression: { } } memberExpr) { return GetQuerySource(memberExpr.Expression, memberExpr); } return null; } /// <summary> /// Evaluates the expression. /// </summary> /// <param name="expr">Expression.</param> /// <typeparam name="T">Expression type.</typeparam> /// <returns>Evaluation result.</returns> public static T EvaluateExpression<T>(Expression expr) { if (expr is ConstantExpression constExpr) { return (T)constExpr.Value!; } // Case for compiled queries: return unchanged. // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull if (expr is ParameterExpression) { return (T) (object) expr; } throw new NotSupportedException("Expression not supported: " + expr); } /// <summary> /// Gets the values from IEnumerable expression. /// </summary> /// <param name="fromExpression">FROM expression.</param> /// <returns>Enumerable items.</returns> public static IEnumerable<object?> EvaluateEnumerableValues(Expression fromExpression) { IEnumerable? result; switch (fromExpression.NodeType) { case ExpressionType.MemberAccess: var memberExpression = (MemberExpression)fromExpression; result = EvaluateExpression<IEnumerable>(memberExpression); break; case ExpressionType.ListInit: var listInitExpression = (ListInitExpression)fromExpression; result = listInitExpression.Initializers .SelectMany(init => init.Arguments) .Select(EvaluateExpression<object>); break; case ExpressionType.NewArrayInit: var newArrayExpression = (NewArrayExpression)fromExpression; result = newArrayExpression.Expressions .Select(EvaluateExpression<object>); break; case ExpressionType.Parameter: // This should happen only when 'IEnumerable.Contains' is called on parameter of compiled query throw new NotSupportedException("'Contains' clause on compiled query parameter is not supported."); default: result = Expression.Lambda(fromExpression).Compile().DynamicInvoke() as IEnumerable; break; } result ??= Enumerable.Empty<object>(); return result .Cast<object>() .ToArray(); } /// <summary> /// Gets the table name with schema. /// <para /> /// Only PUBLIC schema is supported for now by the SQL engine. /// </summary> /// <param name="queryable">Queryable.</param> /// <returns>Table name with schema.</returns> public static string GetTableNameWithSchema(IIgniteQueryableInternal queryable) => $"PUBLIC.{queryable.TableName}"; }