File: System\Data\Mapping\FunctionImportMappingComposable.cs
Project: ndp\fx\src\DataEntity\System.Data.Entity.csproj (System.Data.Entity)
//---------------------------------------------------------------------
// <copyright file="FunctionImportMappingComposable.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// @owner       Microsoft
// @backupOwner willa
//---------------------------------------------------------------------
 
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Common.CommandTrees;
using System.Data.Common.CommandTrees.ExpressionBuilder;
using System.Data.Common.Utils;
using System.Data.Mapping.ViewGeneration;
using System.Data.Metadata.Edm;
using System.Data.Query.InternalTrees;
using System.Data.Query.PlanCompiler;
using System.Diagnostics;
using System.Linq;
 
namespace System.Data.Mapping
{
    /// <summary>
    /// Represents a mapping from a model function import to a store composable function.
    /// </summary>
    internal class FunctionImportMappingComposable : FunctionImportMapping
    {
        #region Constructors
        internal FunctionImportMappingComposable(
            EdmFunction functionImport,
            EdmFunction targetFunction,
            List<Tuple<StructuralType, List<StorageConditionPropertyMapping>, List<StoragePropertyMapping>>> structuralTypeMappings,
            EdmProperty[] targetFunctionKeys,
            StorageMappingItemCollection mappingItemCollection,
            string sourceLocation,
            LineInfo lineInfo) 
            : base(functionImport, targetFunction)
        {
            EntityUtil.CheckArgumentNull(mappingItemCollection, "mappingItemCollection");
            Debug.Assert(functionImport.IsComposableAttribute, "functionImport.IsComposableAttribute");
            Debug.Assert(targetFunction.IsComposableAttribute, "targetFunction.IsComposableAttribute");
            Debug.Assert(functionImport.EntitySet == null || structuralTypeMappings != null, "Function import returning entities must have structuralTypeMappings.");
            Debug.Assert(structuralTypeMappings == null || structuralTypeMappings.Count > 0, "Non-null structuralTypeMappings must not be empty.");
            EdmType resultType;
            Debug.Assert(
                structuralTypeMappings != null ||
                MetadataHelper.TryGetFunctionImportReturnType<EdmType>(functionImport, 0, out resultType) && TypeSemantics.IsScalarType(resultType),
                "Either type mappings should be specified or the function import should be Collection(Scalar).");
            Debug.Assert(functionImport.EntitySet == null || targetFunctionKeys != null, "Keys must be inferred for a function import returning entities.");
            Debug.Assert(targetFunctionKeys == null || targetFunctionKeys.Length > 0, "Keys must be null or non-empty.");
 
            m_mappingItemCollection = mappingItemCollection;
            // We will use these parameters to target s-space function calls in the generated command tree. 
            // Since enums don't exist in s-space we need to use the underlying type.
            m_commandParameters = functionImport.Parameters.Select(p => TypeHelpers.GetPrimitiveTypeUsageForScalar(p.TypeUsage).Parameter(p.Name)).ToArray();
            m_structuralTypeMappings = structuralTypeMappings;
            m_targetFunctionKeys = targetFunctionKeys;
            m_sourceLocation = sourceLocation;
            m_lineInfo = lineInfo;
        }
        #endregion
 
        #region Fields
        private readonly StorageMappingItemCollection m_mappingItemCollection;
        /// <summary>
        /// Command parameter refs created from m_edmFunction parameters.
        /// Used as arguments to target (s-space) function calls in the generated command tree.
        /// </summary>
        private readonly DbParameterReferenceExpression[] m_commandParameters;
        /// <summary>
        /// Result mapping as entity type hierarchy.
        /// </summary>
        private readonly List<Tuple<StructuralType, List<StorageConditionPropertyMapping>, List<StoragePropertyMapping>>> m_structuralTypeMappings;
        /// <summary>
        /// Keys inside the result set of the target function. Inferred based on the mapping (using c-space entity type keys).
        /// </summary>
        private readonly EdmProperty[] m_targetFunctionKeys;
        /// <summary>
        /// ITree template. Requires function argument substitution during function view expansion.
        /// </summary>
        private Node m_internalTreeNode;
        private readonly string m_sourceLocation;
        private readonly LineInfo m_lineInfo;
        #endregion
 
        #region Properties/Methods
        internal EdmProperty[] TvfKeys
        {
            get { return m_targetFunctionKeys; }
        }
 
        #region GetInternalTree(...) implementation
        internal Node GetInternalTree(Command targetIqtCommand, IList<Node> targetIqtArguments)
        {
            if (m_internalTreeNode == null)
            {
                var viewGenErrors = new List<EdmSchemaError>();
                DiscriminatorMap discriminatorMap;
                DbQueryCommandTree tree = GenerateFunctionView(viewGenErrors, out discriminatorMap);
                if (viewGenErrors.Count > 0)
                {
                    throw new MappingException(Helper.CombineErrorMessage(viewGenErrors));
                }
                Debug.Assert(tree != null, "tree != null");
                
                // Convert this into an ITree first
                Command itree = ITreeGenerator.Generate(tree, discriminatorMap);
                var rootProject = itree.Root; // PhysicalProject(RelInput)
                PlanCompiler.Assert(rootProject.Op.OpType == OpType.PhysicalProject, "Expected a physical projectOp at the root of the tree - found " + rootProject.Op.OpType);
                var rootProjectOp = (PhysicalProjectOp)rootProject.Op;
                Debug.Assert(rootProjectOp.Outputs.Count == 1, "rootProjectOp.Outputs.Count == 1");
                var rootInput = rootProject.Child0; // the RelInput in PhysicalProject(RelInput)
 
                // #554756: VarVec enumerators are not cached on the shared Command instance.
                itree.DisableVarVecEnumCaching();
 
                // Function import returns a collection, so convert it to a scalar by wrapping into CollectOp.
                Node relNode = rootInput;
                Var relVar = rootProjectOp.Outputs[0];
                // ProjectOp does not implement Type property, so get the type from the column map.
                TypeUsage functionViewType = rootProjectOp.ColumnMap.Type;
                if (!Command.EqualTypes(functionViewType, this.FunctionImport.ReturnParameter.TypeUsage))
                {
                    Debug.Assert(TypeSemantics.IsPromotableTo(functionViewType, this.FunctionImport.ReturnParameter.TypeUsage), "Mapping expression result type must be promotable to the c-space function return type.");
 
                    // Build "relNode = Project(relNode, SoftCast(relVar))"
                    CollectionType expectedCollectionType = (CollectionType)this.FunctionImport.ReturnParameter.TypeUsage.EdmType;
                    var expectedElementType = expectedCollectionType.TypeUsage;
 
                    Node varRefNode = itree.CreateNode(itree.CreateVarRefOp(relVar));
                    Node castNode = itree.CreateNode(itree.CreateSoftCastOp(expectedElementType), varRefNode);
                    Node varDefListNode = itree.CreateVarDefListNode(castNode, out relVar);
 
                    ProjectOp projectOp = itree.CreateProjectOp(relVar);
                    relNode = itree.CreateNode(projectOp, relNode, varDefListNode);
                }
 
                // Build "Collect(PhysicalProject(relNode))
                m_internalTreeNode = itree.BuildCollect(relNode, relVar);
            }
            Debug.Assert(m_internalTreeNode != null, "m_internalTreeNode != null");
 
            // Prepare argument replacement dictionary
            Debug.Assert(m_commandParameters.Length == targetIqtArguments.Count, "m_commandParameters.Length == targetIqtArguments.Count");
            Dictionary<string, Node> viewArguments = new Dictionary<string, Node>(m_commandParameters.Length);
            for (int i = 0; i < m_commandParameters.Length; ++i)
            {
                var commandParam = (DbParameterReferenceExpression)m_commandParameters[i];
                var argumentNode = targetIqtArguments[i];
 
                // If function import parameter is of enum type, the argument value for it will be of enum type. We however have 
                // converted enum types to underlying types for m_commandParameters. So we now need to softcast the argument 
                // expression to the underlying type as well.
                if (TypeSemantics.IsEnumerationType(argumentNode.Op.Type))
                {
                    argumentNode = targetIqtCommand.CreateNode(
                                        targetIqtCommand.CreateSoftCastOp(TypeHelpers.CreateEnumUnderlyingTypeUsage(argumentNode.Op.Type)),
                                        argumentNode);
                }
 
                Debug.Assert(TypeSemantics.IsPromotableTo(argumentNode.Op.Type, commandParam.ResultType), "Argument type must be promotable to parameter type.");
 
                viewArguments.Add(commandParam.ParameterName, argumentNode);
            }
 
            return FunctionViewOpCopier.Copy(targetIqtCommand, m_internalTreeNode, viewArguments);
        }
 
        private sealed class FunctionViewOpCopier : OpCopier
        {
            private Dictionary<string, Node> m_viewArguments;
 
            private FunctionViewOpCopier(Command cmd, Dictionary<string, Node> viewArguments)
                : base(cmd)
            {
                m_viewArguments = viewArguments;
            }
 
            internal static Node Copy(Command cmd, Node viewNode, Dictionary<string, Node> viewArguments)
            {
                return new FunctionViewOpCopier(cmd, viewArguments).CopyNode(viewNode);
            }
 
            #region Visitor Members
            public override Node Visit(VarRefOp op, Node n)
            {
                // The original function view has store function calls with arguments represented as command parameter refs.
                // We are now replacing command parameter refs with the real argument nodes from the calling tree.
                // The replacement is performed in the function view subtree and we search for parameter refs with names 
                // matching the FunctionImportMapping.FunctionImport parameter names (this is how the command parameters 
                // have been created in the first place, see m_commandParameters and GetCommandTree(...) for more info).
                // The search and replace is not performed on the argument nodes themselves. This is important because it guarantees
                // that we are not replacing unrelated (possibly user-defined) parameter refs that accidentally have the matching names.
                Node argNode;
                if (op.Var.VarType == VarType.Parameter && m_viewArguments.TryGetValue(((ParameterVar)op.Var).ParameterName, out argNode))
                {
                    // Just copy the argNode, do not reapply this visitor. We do not want search and replace inside the argNode. See comment above.
                    return OpCopier.Copy(m_destCmd, argNode);
                }
                else
                {
                    return base.Visit(op, n);
                }
            }
            #endregion
        }
        #endregion
 
        #region GenerateFunctionView(...) implementation
        #region GenerateFunctionView
        internal DbQueryCommandTree GenerateFunctionView(IList<EdmSchemaError> errors, out DiscriminatorMap discriminatorMap)
        {
            Debug.Assert(errors != null, "errors != null");
 
            discriminatorMap = null;
 
            // Prepare the direct call of the store function as StoreFunction(@EdmFunc_p1, ..., @EdmFunc_pN).
            // Note that function call arguments are command parameters created from the m_edmFunction parameters.
            Debug.Assert(this.TargetFunction != null, "this.TargetFunction != null");
            DbExpression storeFunctionInvoke = this.TargetFunction.Invoke(GetParametersForTargetFunctionCall());
 
            // Generate the query expression producing c-space result from s-space function call(s).
            DbExpression queryExpression;
            if (m_structuralTypeMappings != null)
            {
                queryExpression = GenerateStructuralTypeResultMappingView(storeFunctionInvoke, errors, out discriminatorMap);
                Debug.Assert(queryExpression == null ||
                    TypeSemantics.IsPromotableTo(queryExpression.ResultType, this.FunctionImport.ReturnParameter.TypeUsage),
                    "TypeSemantics.IsPromotableTo(queryExpression.ResultType, this.FunctionImport.ReturnParameter.TypeUsage)");
            }
            else
            {
                queryExpression = GenerateScalarResultMappingView(storeFunctionInvoke);
                Debug.Assert(queryExpression == null ||
                    TypeSemantics.IsEqual(queryExpression.ResultType, this.FunctionImport.ReturnParameter.TypeUsage),
                    "TypeSemantics.IsEqual(queryExpression.ResultType, this.FunctionImport.ReturnParameter.TypeUsage)");
            }
 
            if (queryExpression == null)
            {
                // In case of errors during view generation, return.
                return null;
            }
 
            // Generate parameterized command, where command parameters are semantically the c-space function parameters.
            return DbQueryCommandTree.FromValidExpression(m_mappingItemCollection.Workspace, TargetPerspective.TargetPerspectiveDataSpace, queryExpression);
        }
 
        private IEnumerable<DbExpression> GetParametersForTargetFunctionCall()
        {
            Debug.Assert(this.FunctionImport.Parameters.Count == m_commandParameters.Length, "this.FunctionImport.Parameters.Count == m_commandParameters.Length");
            Debug.Assert(this.TargetFunction.Parameters.Count == m_commandParameters.Length, "this.TargetFunction.Parameters.Count == m_commandParameters.Length");
            foreach (var targetParameter in this.TargetFunction.Parameters)
            {
                Debug.Assert(this.FunctionImport.Parameters.Contains(targetParameter.Name), "this.FunctionImport.Parameters.Contains(targetParameter.Name)");
                var functionImportParameter = this.FunctionImport.Parameters.Single(p => p.Name == targetParameter.Name);
                yield return m_commandParameters[this.FunctionImport.Parameters.IndexOf(functionImportParameter)];
            }
        }
 
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] // referenced by System.Data.Entity.Design.dll
        internal void ValidateFunctionView(IList<EdmSchemaError> errors)
        {
            DiscriminatorMap dm;
            GenerateFunctionView(errors, out dm);
        }
        #endregion
 
        #region GenerateStructuralTypeResultMappingView
        private DbExpression GenerateStructuralTypeResultMappingView(DbExpression storeFunctionInvoke, IList<EdmSchemaError> errors, out DiscriminatorMap discriminatorMap)
        {
            Debug.Assert(m_structuralTypeMappings != null && m_structuralTypeMappings.Count > 0, "m_structuralTypeMappings != null && m_structuralTypeMappings.Count > 0");
 
            discriminatorMap = null;
 
            // Process explicit structural type mappings. The mapping is based on the direct call of the store function 
            // wrapped into a projection constructing the mapped structural types.
 
            DbExpression queryExpression = storeFunctionInvoke;
 
            if (m_structuralTypeMappings.Count == 1)
            {
                var mapping = m_structuralTypeMappings[0];
 
                var type = mapping.Item1;
                var conditions = mapping.Item2;
                var propertyMappings = mapping.Item3;
 
                if (conditions.Count > 0)
                {
                    queryExpression = queryExpression.Where((row) => GenerateStructuralTypeConditionsPredicate(conditions, row));
                }
 
                var binding = queryExpression.BindAs("row");
                var entityTypeMappingView = GenerateStructuralTypeMappingView(type, propertyMappings, binding.Variable, errors);
                if (entityTypeMappingView == null)
                {
                    return null;
                }
 
                queryExpression = binding.Project(entityTypeMappingView);
            }
            else
            {
                var binding = queryExpression.BindAs("row");
 
                // Make sure type projection is performed over a closed set where each row is guaranteed to produce a known type.
                // To do this, filter the store function output using the type conditions.
                Debug.Assert(m_structuralTypeMappings.All(m => m.Item2.Count > 0), "In multi-type mapping each type must have conditions.");
                List<DbExpression> structuralTypePredicates = m_structuralTypeMappings.Select(m => GenerateStructuralTypeConditionsPredicate(m.Item2, binding.Variable)).ToList();
                queryExpression = binding.Filter(Helpers.BuildBalancedTreeInPlace(
                    structuralTypePredicates.ToArray(), // clone, otherwise BuildBalancedTreeInPlace will change it
                    (prev, next) => prev.Or(next)));
                binding = queryExpression.BindAs("row");
 
                List<DbExpression> structuralTypeMappingViews = new List<DbExpression>(m_structuralTypeMappings.Count);
                foreach (var mapping in m_structuralTypeMappings)
                {
                    var type = mapping.Item1;
                    var propertyMappings = mapping.Item3;
 
                    var structuralTypeMappingView = GenerateStructuralTypeMappingView(type, propertyMappings, binding.Variable, errors);
                    if (structuralTypeMappingView == null)
                    {
                        continue;
                    }
                    else
                    {
                        structuralTypeMappingViews.Add(structuralTypeMappingView);
                    }
                }
                Debug.Assert(structuralTypeMappingViews.Count == structuralTypePredicates.Count, "structuralTypeMappingViews.Count == structuralTypePredicates.Count");
                if (structuralTypeMappingViews.Count != m_structuralTypeMappings.Count)
                {
                    Debug.Assert(errors.Count > 0, "errors.Count > 0");
                    return null;
                }
 
                // Because we are projecting over the closed set, we can convert the last WHEN THEN into ELSE.
                DbExpression typeConstructors = DbExpressionBuilder.Case(
                    structuralTypePredicates.Take(m_structuralTypeMappings.Count - 1),
                    structuralTypeMappingViews.Take(m_structuralTypeMappings.Count - 1),
                    structuralTypeMappingViews[m_structuralTypeMappings.Count - 1]);
 
                queryExpression = binding.Project(typeConstructors);
 
                if (DiscriminatorMap.TryCreateDiscriminatorMap(this.FunctionImport.EntitySet, queryExpression, out discriminatorMap))
                {
                    Debug.Assert(discriminatorMap != null, "discriminatorMap == null after it has been created");
                }
            }
 
            return queryExpression;
        }
 
        private DbExpression GenerateStructuralTypeMappingView(StructuralType structuralType, List<StoragePropertyMapping> propertyMappings, DbExpression row, IList<EdmSchemaError> errors)
        {
            // Generate property views.
            var properties = TypeHelpers.GetAllStructuralMembers(structuralType);
            Debug.Assert(properties.Count == propertyMappings.Count, "properties.Count == propertyMappings.Count");
            var constructorArgs = new List<DbExpression>(properties.Count);
            for (int i = 0; i < propertyMappings.Count; ++i)
            {
                var propertyMapping = propertyMappings[i];
                Debug.Assert(properties[i].EdmEquals(propertyMapping.EdmProperty), "properties[i].EdmEquals(propertyMapping.EdmProperty)");
                var propertyMappingView = GeneratePropertyMappingView(propertyMapping, row, new List<string>() { propertyMapping.EdmProperty.Name }, errors);
                if (propertyMappingView != null)
                {
                    constructorArgs.Add(propertyMappingView);
                }
            }
            if (constructorArgs.Count != propertyMappings.Count)
            {
                Debug.Assert(errors.Count > 0, "errors.Count > 0");
                return null;
            }
            else
            {
                // Return the structural type constructor.
                return TypeUsage.Create(structuralType).New(constructorArgs);
            }
        }
 
        private DbExpression GenerateStructuralTypeConditionsPredicate(List<StorageConditionPropertyMapping> conditions, DbExpression row)
        {
            Debug.Assert(conditions.Count > 0, "conditions.Count > 0");
            DbExpression predicate = Helpers.BuildBalancedTreeInPlace(conditions.Select(c => GeneratePredicate(c, row)).ToArray(), (prev, next) => prev.And(next));
            return predicate;
        }
 
        private DbExpression GeneratePredicate(StorageConditionPropertyMapping condition, DbExpression row)
        {
            Debug.Assert(condition.EdmProperty == null, "C-side conditions are not supported in function mappings.");
            DbExpression columnRef = GenerateColumnRef(row, condition.ColumnProperty);
 
            if (condition.IsNull.HasValue)
            {
                return condition.IsNull.Value ? (DbExpression)columnRef.IsNull() : (DbExpression)columnRef.IsNull().Not();
            }
            else
            {
                return columnRef.Equal(columnRef.ResultType.Constant(condition.Value));
            }
        }
 
        private DbExpression GeneratePropertyMappingView(StoragePropertyMapping mapping, DbExpression row, List<string> context, IList<EdmSchemaError> errors)
        {
            Debug.Assert(mapping is StorageScalarPropertyMapping, "Complex property mapping is not supported in function imports.");
            var scalarPropertyMapping = (StorageScalarPropertyMapping)mapping;
            return GenerateScalarPropertyMappingView(scalarPropertyMapping.EdmProperty, scalarPropertyMapping.ColumnProperty, row);
        }
 
        private DbExpression GenerateScalarPropertyMappingView(EdmProperty edmProperty, EdmProperty columnProperty, DbExpression row)
        {
            DbExpression accessorExpr = GenerateColumnRef(row, columnProperty);
            if (!TypeSemantics.IsEqual(accessorExpr.ResultType, edmProperty.TypeUsage))
            {
                accessorExpr = accessorExpr.CastTo(edmProperty.TypeUsage);
            }
            return accessorExpr;
        }
 
        private DbExpression GenerateColumnRef(DbExpression row, EdmProperty column)
        {
            Debug.Assert(row.ResultType.EdmType.BuiltInTypeKind == BuiltInTypeKind.RowType, "Input type is expected to be a row type.");
            var rowType = (RowType)row.ResultType.EdmType;
            Debug.Assert(rowType.Properties.Contains(column.Name), "Column name must be resolvable in the TVF result type.");
            return row.Property(column.Name);
        }
        #endregion
 
        #region GenerateScalarResultMappingView
        private DbExpression GenerateScalarResultMappingView(DbExpression storeFunctionInvoke)
        {
            DbExpression queryExpression = storeFunctionInvoke;
 
            CollectionType functionImportReturnType;
            if (!MetadataHelper.TryGetFunctionImportReturnCollectionType(this.FunctionImport, 0, out functionImportReturnType))
            {
                Debug.Fail("Failed to get the result type of the function import.");
            }
 
            Debug.Assert(TypeSemantics.IsCollectionType(queryExpression.ResultType), "Store function must be TVF (collection expected).");
            var collectionType = (CollectionType)queryExpression.ResultType.EdmType;
            Debug.Assert(TypeSemantics.IsRowType(collectionType.TypeUsage), "Store function must be TVF (collection of rows expected).");
            var rowType = (RowType)collectionType.TypeUsage.EdmType;
            var column = rowType.Properties[0];
 
            Func<DbExpression, DbExpression> scalarView = (DbExpression row) =>
            {
                var propertyAccess = row.Property(column);
                if (TypeSemantics.IsEqual(functionImportReturnType.TypeUsage, column.TypeUsage))
                {
                    return propertyAccess;
                }
                else
                {
                    return propertyAccess.CastTo(functionImportReturnType.TypeUsage);
                }
            };
 
            queryExpression = queryExpression.Select(row => scalarView(row));
            return queryExpression;
        }
        #endregion
        #endregion
        #endregion
    }
}