| /* |
| * 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) => queryable.TableName.CanonicalName; |
| } |