//---------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// @owner [....], [....]
// @backupOwner [....]
//---------------------------------------------------------------------
using System.Data.Common.CommandTrees;
using System.Collections.Generic;
using System.Data.Metadata.Edm;
using System.Diagnostics;
using System.Data.Common.Utils;
using System.Linq;
using System.Globalization;
using System.Data.Common.CommandTrees.ExpressionBuilder;
using System.Data.Common.CommandTrees.Internal;
namespace System.Data.Common.CommandTrees.Internal
{
///
/// Utility class that walks a mapping view and returns a simplified expression with projection
/// nodes collapsed. Specifically recognizes the following common pattern in mapping views:
///
/// outerProject(outerBinding(innerProject(innerBinding, innerNew)), outerProjection)
///
/// Recognizes simple disciminator patterns of the form:
///
/// select
/// case when Disc = value1 then value Type1(...)
/// case when Disc = value2 then value Type2(...)
/// ...
///
/// Recognizes redundant case statement of the form:
///
/// select
/// case when (case when Predicate1 then true else false) ...
///
///
internal class ViewSimplifier
{
internal static DbQueryCommandTree SimplifyView(EntitySetBase extent, DbQueryCommandTree view)
{
ViewSimplifier vs = new ViewSimplifier(view.MetadataWorkspace, extent);
view = vs.Simplify(view);
return view;
}
private readonly MetadataWorkspace metadata;
private readonly EntitySetBase extent;
private ViewSimplifier(MetadataWorkspace mws, EntitySetBase viewTarget)
{
this.metadata = mws;
this.extent = viewTarget;
}
private DbQueryCommandTree Simplify(DbQueryCommandTree view)
{
var simplifier = PatternMatchRuleProcessor.Create(
// determines if an expression is of the form outerProject(outerProjection(innerProject(innerNew)))
PatternMatchRule.Create(Pattern_CollapseNestedProjection, ViewSimplifier.CollapseNestedProjection),
// A case statement can potentially be simplified
PatternMatchRule.Create(Pattern_Case, ViewSimplifier.SimplifyCaseStatement),
// Nested TPH discriminator pattern can be converted to the expected TPH discriminator pattern
PatternMatchRule.Create(Pattern_NestedTphDiscriminator, ViewSimplifier.SimplifyNestedTphDiscriminator),
// Entity constructors may be augmented with FK-based related entity refs
PatternMatchRule.Create(Pattern_EntityConstructor, this.AddFkRelatedEntityRefs)
);
DbExpression queryExpression = view.Query;
queryExpression = simplifier(queryExpression);
view = DbQueryCommandTree.FromValidExpression(view.MetadataWorkspace, view.DataSpace, queryExpression);
return view;
}
#region Navigation simplification support by adding FK-based related entity refs
private static readonly Func Pattern_EntityConstructor =
Patterns.MatchProject(
Patterns.AnyExpression,
Patterns.And(
Patterns.MatchEntityType,
Patterns.Or
(
Patterns.MatchNewInstance(),
Patterns.MatchCase(Patterns.AnyExpressions, Patterns.MatchForAll(Patterns.MatchNewInstance()), Patterns.MatchNewInstance())
)
)
);
private bool doNotProcess;
private DbExpression AddFkRelatedEntityRefs(DbExpression viewConstructor)
{
// If the extent being simplified is not a C-Space entity set, or if it has already
// been processed by the simplifier, then keep the original expression by returning
// null.
//
if (this.doNotProcess)
{
return null;
}
if(this.extent.BuiltInTypeKind != BuiltInTypeKind.EntitySet ||
this.extent.EntityContainer.DataSpace != DataSpace.CSpace)
{
this.doNotProcess = true;
return null;
}
// Get a reference to the entity set being simplified, and find all the foreign key
// (foreign key) associations for which the association set references that entity set,
// with either association end.
//
EntitySet targetSet = (EntitySet)this.extent;
var relSets =
targetSet.EntityContainer.BaseEntitySets
.Where(es => es.BuiltInTypeKind == BuiltInTypeKind.AssociationSet)
.Cast()
.Where(assocSet =>
assocSet.ElementType.IsForeignKey &&
assocSet.AssociationSetEnds.Any(se => se.EntitySet == targetSet)
)
.ToList();
// If no foreign key association sets that reference the entity set are present, then
// no further processing is necessary, because FK-based related entity references cannot
// be computed and added to the entities constructed for the entity set.
if (relSets.Count == 0)
{
this.doNotProcess = true;
return null;
}
// For every relationship set that references this entity set, the relationship type and
// foreign key constraint are used to determine if the entity set is the dependent set.
// If it is the dependent set, then it is possible to augment the view definition with a
// related entity ref that represents the navigation of the relationship set's relationship
// from the dependent end (this entity set) to the the principal end (the entity set that
// is referenced by the other association set end of the relationship set).
//
var principalSetsAndDependentTypes = new HashSet>();
foreach (AssociationSet relSet in relSets)
{
// Retrieve the single referential constraint from the foreign key association, and
// use it to determine whether the association set end that represents the dependent
// end of the association references this entity set.
//
var fkConstraint = relSet.ElementType.ReferentialConstraints[0];
var dependentSetEnd = relSet.AssociationSetEnds[fkConstraint.ToRole.Name];
if (dependentSetEnd.EntitySet == targetSet)
{
EntityType requiredSourceNavType = (EntityType)TypeHelpers.GetEdmType(dependentSetEnd.CorrespondingAssociationEndMember.TypeUsage).ElementType;
var principalSetEnd = relSet.AssociationSetEnds[fkConstraint.FromRole.Name];
// Record the entity type that an element of this dependent entity set must have in order
// to be a valid navigation source for the relationship set's relationship, along with the
// association set end for the destination (principal) end of the navigation and the FK
// constraint that is associated with the relationship type. This information may be used
// later to construct a related entity ref for any entity constructor expression in the view
// that produces an entity of the required source type or a subtype.
//
principalSetsAndDependentTypes.Add(Tuple.Create(requiredSourceNavType, principalSetEnd, fkConstraint));
}
}
// If no foreign key association sets that use the entity set as the dependent set are present,
// then no further processing is possible, since FK-based related entity refs can only be added
// to the view definition for navigations from the dependent end of the relationship to the principal.
//
if (principalSetsAndDependentTypes.Count == 0)
{
this.doNotProcess = true;
return null;
}
// This rule supports a view that is capped with a projection of the form
// (input).Project(x => new Entity())
// or
// (input).Project(x => CASE WHEN (condition1) THEN new Entity1() ELSE WHEN (condition2) THEN new Entity2()... ELSE new EntityN())
// where every new instance expression Entity1()...EntityN() constructs an entity of a type
// that is compatible with the entity set's element type.
// Here, the list of all DbNewInstanceExpressions contained in the projection is remembered,
// along with any CASE statement conditions, if present. These expressions will be updated
// if necessary and used to build a new capping projection if any of the entity constructors
// are augmented with FK-based related entity references.
//
DbProjectExpression entityProject = (DbProjectExpression)viewConstructor;
List constructors = new List();
List conditions = null;
if (entityProject.Projection.ExpressionKind == DbExpressionKind.Case)
{
// If the projection is a DbCaseExpression, then every result must be a DbNewInstanceExpression
DbCaseExpression discriminatedConstructor = (DbCaseExpression)entityProject.Projection;
conditions = new List(discriminatedConstructor.When.Count);
for (int idx = 0; idx < discriminatedConstructor.When.Count; idx++)
{
conditions.Add(discriminatedConstructor.When[idx]);
constructors.Add((DbNewInstanceExpression)discriminatedConstructor.Then[idx]);
}
constructors.Add((DbNewInstanceExpression)discriminatedConstructor.Else);
}
else
{
// Otherwise, the projection must be a single DbNewInstanceExpression
constructors.Add((DbNewInstanceExpression)entityProject.Projection);
}
bool rebuildView = false;
for (int idx = 0; idx < constructors.Count; idx++)
{
DbNewInstanceExpression entityConstructor = constructors[idx];
EntityType constructedEntityType = TypeHelpers.GetEdmType(entityConstructor.ResultType);
List relatedRefs =
principalSetsAndDependentTypes
.Where(psdt => constructedEntityType == psdt.Item1 || constructedEntityType.IsSubtypeOf(psdt.Item1))
.Select(psdt => RelatedEntityRefFromAssociationSetEnd(constructedEntityType, entityConstructor, psdt.Item2, psdt.Item3)).ToList();
if (relatedRefs.Count > 0)
{
if (entityConstructor.HasRelatedEntityReferences)
{
relatedRefs = entityConstructor.RelatedEntityReferences.Concat(relatedRefs).ToList();
}
entityConstructor = DbExpressionBuilder.CreateNewEntityWithRelationshipsExpression(constructedEntityType, entityConstructor.Arguments, relatedRefs);
constructors[idx] = entityConstructor;
rebuildView = true;
}
}
// Default to returning null to indicate that this rule did not produce a modified expression
//
DbExpression result = null;
if (rebuildView)
{
// rebuildView is true, so entity constructing DbNewInstanceExpression(s) were encountered
// and updated with additional related entity refs. The DbProjectExpression that caps the
// view definition therefore needs to be rebuilt and returned as the result of this rule.
//
if (conditions != null)
{
// The original view definition projection was a DbCaseExpression.
// The new expression is also a DbCaseExpression that uses the conditions from the
// original expression together with the updated result expressions to produce the
// new capping projection.
//
List whens = new List(conditions.Count);
List thens = new List(conditions.Count);
for (int idx = 0; idx < conditions.Count; idx++)
{
whens.Add(conditions[idx]);
thens.Add(constructors[idx]);
}
result = entityProject.Input.Project(DbExpressionBuilder.Case(whens, thens, constructors[conditions.Count]));
}
else
{
// Otherwise, the capping projection consists entirely of the updated DbNewInstanceExpression.
//
result = entityProject.Input.Project(constructors[0]);
}
}
// Regardless of whether or not the view was updated, this rule should not be applied again during rule processing
this.doNotProcess = true;
return result;
}
private static DbRelatedEntityRef RelatedEntityRefFromAssociationSetEnd(EntityType constructedEntityType, DbNewInstanceExpression entityConstructor, AssociationSetEnd principalSetEnd, ReferentialConstraint fkConstraint)
{
EntityType principalEntityType = (EntityType)TypeHelpers.GetEdmType(fkConstraint.FromRole.TypeUsage).ElementType;
IList principalKeyValues = null;
// Create Entity Property/DbExpression value pairs from the entity constructor DbExpression,
// then join these with the principal/dependent property pairs from the FK constraint
// to produce principal property name/DbExpression value pairs from which to create the principal ref.
//
// Ideally the code would be as below, but anonymous types break asmmeta:
//var keyPropAndValue =
// from pv in constructedEntityType.Properties.Select((p, idx) => new { DependentProperty = p, Value = entityConstructor.Arguments[idx] })
// join ft in fkConstraint.FromProperties.Select((fp, idx) => new { PrincipalProperty = fp, DependentProperty = fkConstraint.ToProperties[idx] })
// on pv.DependentProperty equals ft.DependentProperty
// select new { PrincipalProperty = ft.PrincipalProperty.Name, Value = pv.Value };
//
var keyPropAndValue =
from pv in constructedEntityType.Properties.Select((p, idx) => Tuple.Create(p, entityConstructor.Arguments[idx])) // new { DependentProperty = p, Value = entityConstructor.Arguments[idx] })
join ft in fkConstraint.FromProperties.Select((fp, idx) => Tuple.Create(fp, fkConstraint.ToProperties[idx])) //new { PrincipalProperty = fp, DependentProperty = fkConstraint.ToProperties[idx] })
on pv.Item1 equals ft.Item2 //pv.DependentProperty equals ft.DependentProperty
select Tuple.Create(ft.Item1.Name, pv.Item2); // new { PrincipalProperty = ft.PrincipalProperty.Name, Value = pv.Value };
// If there is only a single property in the principal's key, then there is no ordering concern.
// Otherwise, create a dictionary of principal key property name to DbExpression value so that
// when used as the arguments to the ref expression, the dependent property values - used here
// as principal key property values - are in the correct order, which is the same order as the
// key members themselves.
//
if (fkConstraint.FromProperties.Count == 1)
{
var singleKeyNameAndValue = keyPropAndValue.Single();
Debug.Assert(singleKeyNameAndValue.Item1 == fkConstraint.FromProperties[0].Name, "Unexpected single key property name");
principalKeyValues = new[] { singleKeyNameAndValue.Item2 };
}
else
{
var keyValueMap = keyPropAndValue.ToDictionary(pav => pav.Item1, pav => pav.Item2, StringComparer.Ordinal);
principalKeyValues = principalEntityType.KeyMemberNames.Select(memberName => keyValueMap[memberName]).ToList();
}
// Create the ref to the principal entity based on the (now correctly ordered) key value expressions.
//
DbRefExpression principalRef = principalSetEnd.EntitySet.CreateRef(principalEntityType, principalKeyValues);
DbRelatedEntityRef result = DbExpressionBuilder.CreateRelatedEntityRef(fkConstraint.ToRole, fkConstraint.FromRole, principalRef);
return result;
}
#endregion
#region Nested TPH Discriminator simplification
///
/// Matches the nested TPH discriminator pattern produced by view generation
///
private static readonly Func Pattern_NestedTphDiscriminator =
Patterns.MatchProject(
Patterns.MatchFilter(
Patterns.MatchProject(
Patterns.MatchFilter(
Patterns.AnyExpression,
Patterns.Or(
Patterns.MatchKind(DbExpressionKind.Equals),
Patterns.MatchKind(DbExpressionKind.Or)
)
),
Patterns.And(
Patterns.MatchRowType,
Patterns.MatchNewInstance(
Patterns.MatchForAll(
Patterns.Or(
Patterns.And(
Patterns.MatchNewInstance(),
Patterns.MatchComplexType
),
Patterns.MatchKind(DbExpressionKind.Property),
Patterns.MatchKind(DbExpressionKind.Case)
)
)
)
)
),
Patterns.Or(
Patterns.MatchKind(DbExpressionKind.Property),
Patterns.MatchKind(DbExpressionKind.Or)
)
),
Patterns.And(
Patterns.MatchEntityType,
Patterns.MatchCase(
Patterns.MatchForAll(Patterns.MatchKind(DbExpressionKind.Property)),
Patterns.MatchForAll(Patterns.MatchKind(DbExpressionKind.NewInstance)),
Patterns.MatchKind(DbExpressionKind.NewInstance)
)
)
);
///
/// Converts the DbExpression equivalent of:
///
/// SELECT CASE
/// WHEN a._from0 THEN SUBTYPE1()
/// ...
/// WHEN a._from[n-2] THEN SUBTYPE_n-1()
/// ELSE SUBTYPE_n
/// FROM
/// SELECT
/// b.C1..., b.Cn
/// CASE WHEN b.Discriminator = SUBTYPE1_Value THEN true ELSE false AS _from0
/// ...
/// CASE WHEN b.Discriminator = SUBTYPE_n_Value THEN true ELSE false AS _from[n-1]
/// FROM TSet AS b
/// WHERE b.Discriminator = SUBTYPE1_Value... OR x.Discriminator = SUBTYPE_n_Value
/// AS a
/// WHERE a._from0... OR a._from[n-1]
///
/// into the DbExpression equivalent of the following, which is matched as a TPH discriminator
/// by the class and so allows a
/// to be produced for the view, which would not otherwise be possible. Note that C1 through Cn
/// are only allowed to be scalars or complex type constructors based on direct property references
/// to the store entity set's scalar properties.
///
/// SELECT CASE
/// WHEN y.Discriminator = SUBTTYPE1_Value THEN SUBTYPE1()
/// ...
/// WHEN y.Discriminator = SUBTYPE_n-1_Value THEN SUBTYPE_n-1()
/// ELSE SUBTYPE_n()
/// FROM
/// SELECT x.C1..., x.Cn, Discriminator FROM TSet AS x
/// WHERE x.Discriminator = SUBTYPE1_Value... OR x.Discriminator = SUBTYPE_n_Value
/// AS y
///
///
private static DbExpression SimplifyNestedTphDiscriminator(DbExpression expression)
{
DbProjectExpression entityProjection = (DbProjectExpression)expression;
DbFilterExpression booleanColumnFilter = (DbFilterExpression)entityProjection.Input.Expression;
DbProjectExpression rowProjection = (DbProjectExpression)booleanColumnFilter.Input.Expression;
DbFilterExpression discriminatorFilter = (DbFilterExpression)rowProjection.Input.Expression;
List predicates = FlattenOr(booleanColumnFilter.Predicate).ToList();
List propertyPredicates =
predicates.OfType()
.Where(px => px.Instance.ExpressionKind == DbExpressionKind.VariableReference &&
((DbVariableReferenceExpression)px.Instance).VariableName == booleanColumnFilter.Input.VariableName).ToList();
if (predicates.Count != propertyPredicates.Count)
{
return null;
}
List predicateColumnNames = propertyPredicates.Select(px => px.Property.Name).ToList();
Dictionary