//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @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 { /// /// Represents a mapping from a model function import to a store composable function. /// internal class FunctionImportMappingComposable : FunctionImportMapping { #region Constructors internal FunctionImportMappingComposable( EdmFunction functionImport, EdmFunction targetFunction, List, List>> 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(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; /// /// Command parameter refs created from m_edmFunction parameters. /// Used as arguments to target (s-space) function calls in the generated command tree. /// private readonly DbParameterReferenceExpression[] m_commandParameters; /// /// Result mapping as entity type hierarchy. /// private readonly List, List>> m_structuralTypeMappings; /// /// Keys inside the result set of the target function. Inferred based on the mapping (using c-space entity type keys). /// private readonly EdmProperty[] m_targetFunctionKeys; /// /// ITree template. Requires function argument substitution during function view expansion. /// 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 targetIqtArguments) { if (m_internalTreeNode == null) { var viewGenErrors = new List(); 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 viewArguments = new Dictionary(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 m_viewArguments; private FunctionViewOpCopier(Command cmd, Dictionary viewArguments) : base(cmd) { m_viewArguments = viewArguments; } internal static Node Copy(Command cmd, Node viewNode, Dictionary 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 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 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 errors) { DiscriminatorMap dm; GenerateFunctionView(errors, out dm); } #endregion #region GenerateStructuralTypeResultMappingView private DbExpression GenerateStructuralTypeResultMappingView(DbExpression storeFunctionInvoke, IList 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 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 structuralTypeMappingViews = new List(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 propertyMappings, DbExpression row, IList errors) { // Generate property views. var properties = TypeHelpers.GetAllStructuralMembers(structuralType); Debug.Assert(properties.Count == propertyMappings.Count, "properties.Count == propertyMappings.Count"); var constructorArgs = new List(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() { 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 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 context, IList 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 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 } }