//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner [....] // @backupOwner [....] //--------------------------------------------------------------------- using System; using System.Data.Common.CommandTrees; using System.Data.Common.CommandTrees.ExpressionBuilder; using System.Data.Common.Utils; using System.Collections.Generic; using System.Text; using System.Diagnostics; using System.Data.Mapping.ViewGeneration.CqlGeneration; using System.Data.Metadata.Edm; using System.Linq; namespace System.Data.Mapping.ViewGeneration.Structures { /// /// A class that corresponds to a path in some extent, e.g., Person, Person.addr, Person.addr.state /// Empty path represents path to the extent. /// internal sealed class MemberPath : InternalBase, IEquatable { #region Fields /// /// The base entity set. /// private readonly EntitySetBase m_extent; /// /// List of members in the path. /// private readonly List m_path; internal static readonly IEqualityComparer EqualityComparer = new Comparer(); #endregion #region Constructors /// /// Creates a member path that corresponds to in the (or the extent itself). /// internal MemberPath(EntitySetBase extent, IEnumerable path) { m_extent = extent; m_path = path.ToList(); } /// /// Creates a member path that corresponds to the . /// internal MemberPath(EntitySetBase extent) : this(extent, Enumerable.Empty()) { } /// /// Creates a path corresponding to . /// internal MemberPath(EntitySetBase extent, EdmMember member) : this(extent, Enumerable.Repeat(member, 1)) { } /// /// Creates a member path corresponding to the path . /// internal MemberPath(MemberPath prefix, EdmMember last) { m_extent = prefix.m_extent; m_path = new List(prefix.m_path); m_path.Add(last); } #endregion #region Properties /// /// Returns the first path item in a non-empty path, otherwise null. /// internal EdmMember RootEdmMember { get { return m_path.Count > 0 ? m_path[0] : null; } } /// /// Returns the last path item in a non-empty path, otherwise null. /// internal EdmMember LeafEdmMember { get { return m_path.Count > 0 ? m_path[m_path.Count - 1] : null; } } /// /// For non-empty paths returns name of the last path item, otherwise returns name of . /// internal string LeafName { get { if (m_path.Count == 0) { return m_extent.Name; } else { return LeafEdmMember.Name; } } } /// /// Tells path represents a computed slot. /// internal bool IsComputed { get { if (m_path.Count == 0) { return false; } else { return RootEdmMember.IsStoreGeneratedComputed; } } } /// /// Returns the default value the slot represented by the path. If no default value is present, returns null. /// internal object DefaultValue { get { if (m_path.Count == 0) { return null; } Facet facet; if (LeafEdmMember.TypeUsage.Facets.TryGetValue(EdmProviderManifest.DefaultValueFacetName, false, out facet)) { return facet.Value; } return null; } } /// /// Returns true if slot represented by the path is part of a key. /// internal bool IsPartOfKey { get { if (m_path.Count == 0) { return false; } return MetadataHelper.IsPartOfEntityTypeKey(LeafEdmMember); } } /// /// Returns true if slot represented by the path is nullable. /// internal bool IsNullable { get { if (m_path.Count == 0) { return false; } return MetadataHelper.IsMemberNullable(LeafEdmMember); } } /// /// If path corresponds to an entity set (empty path) or an association end ( is as association set, and path length is 1), /// returns associated with the value of the slot represented by this path, otherwise returns null. /// internal EntitySet EntitySet { get { if (m_path.Count == 0) { return m_extent as EntitySet; } else if (m_path.Count == 1) { AssociationEndMember endMember = this.RootEdmMember as AssociationEndMember; if (endMember != null) { EntitySet result = MetadataHelper.GetEntitySetAtEnd((AssociationSet)m_extent, endMember); return result; } } return null; } } /// /// Extent of the path. /// internal EntitySetBase Extent { get { return m_extent; } } /// /// Returns the type of attribute denoted by the path. /// For example, member type of Person.addr.zip would be integer. For extent, it is the element type. /// internal EdmType EdmType { get { if (m_path.Count > 0) { return LeafEdmMember.TypeUsage.EdmType; } else { return m_extent.ElementType; } } } /// /// Returns Cql field alias generated from the path items. /// internal string CqlFieldAlias { get { string alias = PathToString(true); if (false == alias.Contains("_")) { // if alias of the member does not contain any "_", we can replace "." with "_" so that we can get a simple identifier. alias = alias.Replace('.', '_'); } StringBuilder builder = new StringBuilder(); CqlWriter.AppendEscapedName(builder, alias); return builder.ToString(); } } #endregion #region Methods /// /// Returns false iff the path is /// * A descendant of some nullable property /// * A descendant of an optional composition/collection /// * A descendant of a property that does not belong to the basetype/rootype of its parent. /// internal bool IsAlwaysDefined(Dictionary> inheritanceGraph) { if (m_path.Count == 0) { // Extents are always defined return true; } EdmMember member = m_path.Last(); //Dont check last member, thats the property we are testing for (int i = 0; i < m_path.Count - 1; i++) { EdmMember current = m_path[i]; // If member is nullable then "this" will not always be defined if (MetadataHelper.IsMemberNullable(current)) { return false; } } //Now check if there are any concrete types other than all subtypes of Type defining this member //by definition association types member are always present since they are IDs if (m_path[0].DeclaringType is AssociationType) { return true; } EntityType entitySetType = m_extent.ElementType as EntityType; if (entitySetType == null) //association type { return true; } //well, we handle the first case because we don't knwo how to get to subtype (i.e. the edge to avoid) EntityType memberDeclaringType = m_path[0].DeclaringType as EntityType; EntityType parentType = memberDeclaringType.BaseType as EntityType; if (entitySetType.EdmEquals(memberDeclaringType) || MetadataHelper.IsParentOf(memberDeclaringType, entitySetType) || parentType == null) { return true; } else if (!parentType.Abstract && !MetadataHelper.DoesMemberExist(parentType, member)) { return false; } bool result = !RecurseToFindMemberAbsentInConcreteType(parentType, memberDeclaringType, member, entitySetType, inheritanceGraph); return result; } private static bool RecurseToFindMemberAbsentInConcreteType(EntityType current, EntityType avoidEdge, EdmMember member, EntityType entitySetType, Dictionary> inheritanceGraph) { Set edges = inheritanceGraph[current]; //for each outgoing edge (from current) where the edge is not the one to avoid, // navigate depth-first foreach (var edge in edges.Where(type => !type.EdmEquals(avoidEdge))) { //Dont traverse above the EntitySet's Element type if (entitySetType.BaseType != null && entitySetType.BaseType.EdmEquals(edge)) { continue; } if (!edge.Abstract && !MetadataHelper.DoesMemberExist(edge, member)) { //found it.. I'm the concrete type that has member absent. return true; } if (RecurseToFindMemberAbsentInConcreteType(edge, current /*avoid traversing down back here*/, member, entitySetType, inheritanceGraph)) { //one of the edges reachable from me found it return true; } } //no body found this counter example return false; } /// /// Determines all the identifiers used in the path and adds them to . /// internal void GetIdentifiers(CqlIdentifiers identifiers) { // Get the extent name and extent type name identifiers.AddIdentifier(m_extent.Name); identifiers.AddIdentifier(m_extent.ElementType.Name); foreach (EdmMember member in m_path) { identifiers.AddIdentifier(member.Name); } } /// /// Returns true iff all members are nullable properties, i.e., if even one of them is non-nullable, returns false. /// internal static bool AreAllMembersNullable(IEnumerable members) { foreach (MemberPath path in members) { if (path.m_path.Count == 0) { return false; // Extents are not nullable } if (path.IsNullable == false) { return false; } } return true; } /// /// Returns a string that has the list of properties in (i.e., just the last name) if is false. /// Else the is added. /// internal static string PropertiesToUserString(IEnumerable members, bool fullPath) { bool isFirst = true; StringBuilder builder = new StringBuilder(); foreach (MemberPath path in members) { if (isFirst == false) { builder.Append(", "); } isFirst = false; if (fullPath) { builder.Append(path.PathToString(false)); } else { builder.Append(path.LeafName); } } return builder.ToString(); } /// /// Given a member path and an alias, returns an eSQL string correspondng to the fully-qualified name .path, e.g., T1.Address.Phone.Zip. /// If a subcomponent belongs to subclass, generates a treat for it, e.g. "TREAT(T1 as Customer).Address". /// Or even "TREAT(TREAT(T1 AS Customer).Address as USAddress).Zip". /// internal StringBuilder AsEsql(StringBuilder inputBuilder, string blockAlias) { // Due to the TREAT stuff, we cannot build incrementally. // So we use a local StringBuilder - it should not be that inefficient (one extra copy). StringBuilder builder = new StringBuilder(); // Add blockAlias as a starting point for blockAlias.member1.member2... CqlWriter.AppendEscapedName(builder, blockAlias); // Process all items in the path. AsCql( // accessMember action (memberName) => { builder.Append('.'); CqlWriter.AppendEscapedName(builder, memberName); }, // getKey action () => { builder.Insert(0, "Key("); builder.Append(")"); }, // treatAs action (treatAsType) => { builder.Insert(0, "TREAT("); builder.Append(" AS "); CqlWriter.AppendEscapedTypeName(builder, treatAsType); builder.Append(')'); }); inputBuilder.Append(builder.ToString()); return inputBuilder; } internal DbExpression AsCqt(DbExpression row) { DbExpression cqt = row; // Process all items in the path. AsCql( // accessMember action (memberName) => { cqt = DbExpressionBuilder.Property(cqt, memberName); }, // getKey action () => { cqt = cqt.GetRefKey(); }, // treatAs action (treatAsType) => { var typeUsage = TypeUsage.Create(treatAsType); cqt = cqt.TreatAs(typeUsage); }); return cqt; } internal void AsCql(Action accessMember, Action getKey, Action treatAs) { // Keep track of the previous type so that we can determine if we need to cast or not. EdmType prevType = m_extent.ElementType; foreach (EdmMember member in m_path) { // If prevType is a ref (e.g., ref to CPerson), we need to get the type that it is pointing to and then look for this member in that type. StructuralType prevStructuralType; RefType prevRefType; if (Helper.IsRefType(prevType)) { prevRefType = (RefType)prevType; prevStructuralType = prevRefType.ElementType; } else { prevRefType = null; prevStructuralType = (StructuralType)prevType; } // Check whether the prevType has the present member in it. // If not, we will need to cast the prev type to the appropriate subtype. bool found = MetadataHelper.DoesMemberExist(prevStructuralType, member); if (prevRefType != null) { // For reference types, the key must be present in the element type itself. // E.g., if we have Ref(CPerson), the key must be present as CPerson.pid or CPerson.Address.Phone.Number (i.e., in a complex type). // Note that it cannot be present in the subtype of address or phone either, i.e., this path better not have any TREATs. // We are at CPerson right now. So if we say Key(CPerson), we will get a row with all the key elements. // Then we can continue going down the path in CPerson Debug.Assert(found == true, "We did not find the key property in a ref's element type - it cannot be in a subtype"); Debug.Assert(MetadataHelper.IsPartOfEntityTypeKey(member), "Member is expected to be a key property"); // Emit KEY(current path segment) getKey(); } else if (false == found) { // Need to add Treat(... as ...) expression in the beginning. // Note that it does handle cases like TREAT(TREAT(T1 AS Customer).Address as USAddress).Zip Debug.Assert(prevRefType == null, "We do not allow subtyping in key extraction from Refs"); // Emit TREAT(current path segment as member.DeclaringType) treatAs(member.DeclaringType); } // Add the member's access. We had a path "T1.A.B" till now. accessMember(member.Name); prevType = member.TypeUsage.EdmType; } } public bool Equals(MemberPath right) { return EqualityComparer.Equals(this, right); } public override bool Equals(object obj) { MemberPath right = obj as MemberPath; if (obj == null) { return false; } return Equals(right); } public override int GetHashCode() { return EqualityComparer.GetHashCode(this); } /// /// Returns true if the member denoted by the path corresponds to a scalar (primitive or enum). /// internal bool IsScalarType() { return this.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType || this.EdmType.BuiltInTypeKind == BuiltInTypeKind.EnumType; } internal static IEnumerable GetKeyMembers(EntitySetBase extent, MemberDomainMap domainMap) { MemberPath extentPath = new MemberPath(extent); List keyAttributes = new List( extentPath.GetMembers(extentPath.Extent.ElementType, null /* isScalar */, null /* isConditional */, true /* isPartOfKey */, domainMap)); Debug.Assert(keyAttributes.Any(), "No key attributes?"); return keyAttributes; } internal IEnumerable GetMembers(EdmType edmType, bool? isScalar, bool? isConditional, bool? isPartOfKey, MemberDomainMap domainMap) { MemberPath currentPath = this; StructuralType structuralType = (StructuralType)edmType; foreach (EdmMember edmMember in structuralType.Members) { if (edmMember is AssociationEndMember) { // get end's keys foreach (MemberPath endKey in new MemberPath(currentPath, edmMember).GetMembers( ((RefType)edmMember.TypeUsage.EdmType).ElementType, isScalar, isConditional, true /*isPartOfKey*/, domainMap)) { yield return endKey; } } bool isActuallyScalar = MetadataHelper.IsNonRefSimpleMember(edmMember); if (isScalar == null || isScalar == isActuallyScalar) { EdmProperty childProperty = edmMember as EdmProperty; if (childProperty != null) { bool isActuallyKey = MetadataHelper.IsPartOfEntityTypeKey(childProperty); if (isPartOfKey == null || isPartOfKey == isActuallyKey) { MemberPath childPath = new MemberPath(currentPath, childProperty); bool isActuallyConditional = domainMap.IsConditionMember(childPath); if (isConditional == null || isConditional == isActuallyConditional) { yield return childPath; } } } } } } /// /// Returns true if this path and are equivalent on the C-side via a referential constraint. /// internal bool IsEquivalentViaRefConstraint(MemberPath path1) { MemberPath path0 = this; // Now check if they are equivalent via referential constraint // For example, // * Person.pid and PersonAddress.Person.pid are equivalent // * Person.pid and PersonAddress.Address.pid are equivalent // * Person.pid and Address.pid are equivalent if there is a referential constraint // * PersonAddress.Person.pid and PersonAddress.Address.pid are // equivalent if there is a referential constraint // In short, Person.pid, Address.pid, PersonAddress.Address.pid, // PersonAddress.Person.pid are the same if (path0.EdmType is EntityTypeBase || path1.EdmType is EntityTypeBase || MetadataHelper.IsNonRefSimpleMember(path0.LeafEdmMember) == false || MetadataHelper.IsNonRefSimpleMember(path1.LeafEdmMember) == false) { // If the path corresponds to a top level extent only, ignore // it. Or if it is not a scalar return false; } AssociationSet assocSet0 = path0.Extent as AssociationSet; AssociationSet assocSet1 = path1.Extent as AssociationSet; EntitySet entitySet0 = path0.Extent as EntitySet; EntitySet entitySet1 = path1.Extent as EntitySet; bool result = false; if (assocSet0 != null && assocSet1 != null) { // PersonAddress.Person.pid and PersonAddress.Address.pid case // Check if they are the same association or not if (assocSet0.Equals(assocSet1) == false) { return false; } result = AreAssocationEndPathsEquivalentViaRefConstraint(path0, path1, assocSet0); } else if (entitySet0 != null && entitySet1 != null) { // Person.pid, Address.pid case // Find all the associations between the two sets. If the // fields are equivalent via any association + referential // constraint, return true List assocSets = MetadataHelper.GetAssociationsForEntitySets(entitySet0, entitySet1); foreach (AssociationSet assocSet in assocSets) { // For Person.pid, get PersonAddress.Person.pid or MemberPath assocEndPath0 = path0.GetCorrespondingAssociationPath(assocSet); MemberPath assocEndPath1 = path1.GetCorrespondingAssociationPath(assocSet); if (AreAssocationEndPathsEquivalentViaRefConstraint(assocEndPath0, assocEndPath1, assocSet)) { result = true; break; } } } else { // One of them is an assocSet and the other is an entity set AssociationSet assocSet = assocSet0 != null ? assocSet0 : assocSet1; EntitySet entitySet = entitySet0 != null ? entitySet0 : entitySet1; Debug.Assert(assocSet != null && entitySet != null, "One set must be association and the other must be entity set"); MemberPath assocEndPathA = path0.Extent is AssociationSet ? path0 : path1; MemberPath entityPath = path0.Extent is EntitySet ? path0 : path1; MemberPath assocEndPathB = entityPath.GetCorrespondingAssociationPath(assocSet); if (assocEndPathB == null) { //An EntitySet might participate in multiple AssociationSets //and this might not be the association set that defines the expected referential //constraint //Return false since this does not have any referential constraint specified result = false; } else { result = AreAssocationEndPathsEquivalentViaRefConstraint(assocEndPathA, assocEndPathB, assocSet); } } return result; } /// /// Returns true if and are equivalent via a referential constraint in . /// Requires: and correspond to paths in . /// private static bool AreAssocationEndPathsEquivalentViaRefConstraint(MemberPath assocPath0, MemberPath assocPath1, AssociationSet assocSet) { Debug.Assert(assocPath0.Extent.Equals(assocSet) && assocPath1.Extent.Equals(assocSet), "Extent for paths must be assocSet"); AssociationEndMember end0 = assocPath0.RootEdmMember as AssociationEndMember; AssociationEndMember end1 = assocPath1.RootEdmMember as AssociationEndMember; EdmProperty property0 = assocPath0.LeafEdmMember as EdmProperty; EdmProperty property1 = assocPath1.LeafEdmMember as EdmProperty; if (end0 == null || end1 == null || property0 == null || property1 == null) { return false; } // Now check if these fields are connected via a referential constraint AssociationType assocType = assocSet.ElementType; bool foundConstraint = false; foreach (ReferentialConstraint constraint in assocType.ReferentialConstraints) { bool isFrom0 = end0.Name == constraint.FromRole.Name && end1.Name == constraint.ToRole.Name; bool isFrom1 = end1.Name == constraint.FromRole.Name && end0.Name == constraint.ToRole.Name; if (isFrom0 || isFrom1) { // Found an RI for the two sets. Make sure that the properties are at the same ordinal // isFrom0 is true when end0 corresponds to FromRole and end1 to ToRole ReadOnlyMetadataCollection properties0 = isFrom0 ? constraint.FromProperties : constraint.ToProperties; ReadOnlyMetadataCollection properties1 = isFrom0 ? constraint.ToProperties : constraint.FromProperties; int indexForPath0 = properties0.IndexOf(property0); int indexForPath1 = properties1.IndexOf(property1); if (indexForPath0 == indexForPath1 && indexForPath0 != -1) { foundConstraint = true; break; } } } return foundConstraint; } /// /// Returns the member path corresponding to that field in the . E.g., given Address.pid, returns PersonAddress.Address.pid. /// For self-associations, such as ManagerEmployee with referential constraints (and we have /// [ManagerEmployee.Employee.mid, ManagerEmployee.Employee.eid, ManagerEmployee.Manager.mid]), given Employee.mid, returns /// ManagerEmployee.Employee.mid or ManagerEmployee.Manager.mid /// /// Note: the path need not correspond to a key field of an entity set . /// private MemberPath GetCorrespondingAssociationPath(AssociationSet assocSet) { Debug.Assert(this.Extent is EntitySet, "path must be in the context of an entity set"); // Find the end corresponding to the entity set AssociationEndMember end = MetadataHelper.GetSomeEndForEntitySet(assocSet, (EntitySet)m_extent); // An EntitySet might participate in multiple AssociationSets and // this might not be the association set that defines the expected referential constraint. if (end == null) { return null; } // Create the new members using the end List newMembers = new List(); newMembers.Add(end); newMembers.AddRange(m_path); // The extent is the assocSet MemberPath result = new MemberPath(assocSet, newMembers); return result; } /// /// If member path identifies a relationship end, return its scope. Otherwise, returns null. /// internal EntitySet GetScopeOfRelationEnd() { if (m_path.Count == 0) { return null; } AssociationEndMember relationEndMember = LeafEdmMember as AssociationEndMember; if (relationEndMember == null) { return null; } // Yes, it's a reference, determine its entity set refScope AssociationSet associationSet = (AssociationSet)m_extent; EntitySet result = MetadataHelper.GetEntitySetAtEnd(associationSet, relationEndMember); return result; } /// /// Returns a string of the form "a.b.c" that corresponds to the items in the path. This string can be used for tests or localization. /// If =true, we return a string that is relevant for Cql aliases, else we return the exact path. /// internal string PathToString(bool? forAlias) { StringBuilder builder = new StringBuilder(); if (forAlias != null) { if (forAlias == true) { // For the 0th entry, we just choose the type of the element in // which the first entry belongs, e.g., if Addr belongs to CCustomer, // we choose CCustomer and not CPerson. if (m_path.Count == 0) { EntityTypeBase type = m_extent.ElementType; return type.Name; } builder.Append(m_path[0].DeclaringType.Name); // Get CCustomer here } else { // Append the extent name builder.Append(m_extent.Name); } } // Just join the path using "." for (int i = 0; i < m_path.Count; i++) { builder.Append('.'); builder.Append(m_path[i].Name); } return builder.ToString(); } /// /// Returns a human-readable string corresponding to the path. /// internal override void ToCompactString(StringBuilder builder) { builder.Append(PathToString(false)); } internal void ToCompactString(StringBuilder builder, string instanceToken) { builder.Append(instanceToken + PathToString(null)); } #endregion #region Comparer private sealed class Comparer : IEqualityComparer { public bool Equals(MemberPath left, MemberPath right) { if (object.ReferenceEquals(left, right)) { return true; } // One of them is non-null at least. So if the other one is // null, we cannot be equal if (left == null || right == null) { return false; } // Both are non-null at this point // Checks that the paths are equal component-wise if (left.m_extent.Equals(right.m_extent) == false || left.m_path.Count != right.m_path.Count) { return false; } for (int i = 0; i < left.m_path.Count; i++) { // Comparing MemberMetadata -- can use Equals if (false == left.m_path[i].Equals(right.m_path[i])) { return false; } } return true; } public int GetHashCode(MemberPath key) { int result = key.m_extent.GetHashCode(); foreach (EdmMember member in key.m_path) { result ^= member.GetHashCode(); } return result; } } #endregion } }