internal static void FoldFetchXmlColumns(IDataExecutionPlanNode source, List <SelectColumn> columnSet, IDictionary <string, DataSource> dataSources, IDictionary <string, Type> parameterTypes) { if (source is FetchXmlScan fetchXml) { if (!dataSources.TryGetValue(fetchXml.DataSource, out var dataSource)) { throw new NotSupportedQueryFragmentException("Missing datasource " + fetchXml.DataSource); } // Check if there are any aliases we can apply to the source FetchXml var schema = fetchXml.GetSchema(dataSources, parameterTypes); var processedSourceColumns = new HashSet <string>(StringComparer.OrdinalIgnoreCase); var hasStar = columnSet.Any(col => col.AllColumns && col.SourceColumn == null); var aliasStars = new HashSet <string>(columnSet.Where(col => col.AllColumns && col.SourceColumn != null).Select(col => col.SourceColumn.Replace(".*", "")).Distinct(StringComparer.OrdinalIgnoreCase), StringComparer.OrdinalIgnoreCase); foreach (var col in columnSet) { if (col.AllColumns) { if (col.SourceColumn == null) { // Add an all-attributes to the main entity and all link-entities fetchXml.Entity.AddItem(new allattributes()); foreach (var link in fetchXml.Entity.GetLinkEntities()) { if (link.SemiJoin) { continue; } link.AddItem(new allattributes()); } } else if (!hasStar) { // Only add an all-attributes to the appropriate entity/link-entity if (col.SourceColumn.Replace(".*", "").Equals(fetchXml.Alias, StringComparison.OrdinalIgnoreCase)) { fetchXml.Entity.AddItem(new allattributes()); } else { var link = fetchXml.Entity.FindLinkEntity(col.SourceColumn.Replace(".*", "")); link.AddItem(new allattributes()); } } } else if (!hasStar) { // Only fold individual columns down to the FetchXML if there is no corresponding all-attributes var parts = col.SourceColumn.Split('.'); if (parts.Length == 1 || !aliasStars.Contains(parts[0])) { var sourceCol = col.SourceColumn; schema.ContainsColumn(sourceCol, out sourceCol); var attr = fetchXml.AddAttribute(sourceCol, null, dataSource.Metadata, out var added, out var linkEntity); // Check if we can fold the alias down to the FetchXML too. Don't do this if the name isn't valid for FetchXML if (sourceCol != col.SourceColumn) { parts = col.SourceColumn.Split('.'); } if (!col.OutputColumn.Equals(parts.Last(), StringComparison.OrdinalIgnoreCase) && FetchXmlScan.IsValidAlias(col.OutputColumn)) { if (added || (!processedSourceColumns.Contains(sourceCol) && !fetchXml.IsAliasReferenced(attr.alias))) { // Don't fold the alias if there's also a sort on the same attribute, as it breaks paging // https://markcarrington.dev/2019/12/10/inside-fetchxml-pt-4-order/#sorting_&_aliases var items = linkEntity?.Items ?? fetchXml.Entity.Items; if (items == null || !items.OfType <FetchOrderType>().Any(order => order.attribute == attr.name) || !fetchXml.AllPages) { attr.alias = col.OutputColumn; } } col.SourceColumn = sourceCol.Split('.')[0] + "." + (attr.alias ?? attr.name); } processedSourceColumns.Add(sourceCol); } } } } }
public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints) { LeftSource = LeftSource.FoldQuery(dataSources, options, parameterTypes, hints); LeftSource.Parent = this; RightSource = RightSource.FoldQuery(dataSources, options, parameterTypes, hints); RightSource.Parent = this; if (SemiJoin) { return(this); } var leftSchema = LeftSource.GetSchema(dataSources, parameterTypes); var rightSchema = RightSource.GetSchema(dataSources, parameterTypes); if (LeftSource is FetchXmlScan leftFetch && RightSource is FetchXmlScan rightFetch) { // Can't join data from different sources if (!leftFetch.DataSource.Equals(rightFetch.DataSource, StringComparison.OrdinalIgnoreCase)) { return(this); } // If one source is distinct and the other isn't, joining the two won't produce the expected results if (leftFetch.FetchXml.distinct ^ rightFetch.FetchXml.distinct) { return(this); } // Check that the alias is valid for FetchXML if (!FetchXmlScan.IsValidAlias(rightFetch.Alias)) { return(this); } var leftEntity = leftFetch.Entity; var rightEntity = rightFetch.Entity; // Check that the join is on columns that are available in the FetchXML var leftAttribute = LeftAttribute.GetColumnName(); if (!leftSchema.ContainsColumn(leftAttribute, out leftAttribute)) { return(this); } var rightAttribute = RightAttribute.GetColumnName(); if (!rightSchema.ContainsColumn(rightAttribute, out rightAttribute)) { return(this); } var leftAttributeParts = leftAttribute.Split('.'); var rightAttributeParts = rightAttribute.Split('.'); if (leftAttributeParts.Length != 2) { return(this); } if (rightAttributeParts.Length != 2) { return(this); } // If the entities are from different virtual entity data providers it's probably not going to work if (!dataSources.TryGetValue(leftFetch.DataSource, out var dataSource)) { throw new NotSupportedQueryFragmentException("Missing datasource " + leftFetch.DataSource); } if (dataSource.Metadata[leftFetch.Entity.name].DataProviderId != dataSource.Metadata[rightFetch.Entity.name].DataProviderId) { return(this); } // Check we're not going to have too many link entities var leftLinkCount = leftFetch.Entity.GetLinkEntities().Count(); var rightLinkCount = rightFetch.Entity.GetLinkEntities().Count() + 1; if (leftLinkCount + rightLinkCount > 10) { return(this); } // If we're doing a right outer join, switch everything round to do a left outer join // Also switch join order for inner joins to use N:1 relationships instead of 1:N to avoid problems with paging if (JoinType == QualifiedJoinType.RightOuter || JoinType == QualifiedJoinType.Inner && !rightAttributeParts[0].Equals(rightFetch.Alias, StringComparison.OrdinalIgnoreCase) || JoinType == QualifiedJoinType.Inner && leftAttribute == leftSchema.PrimaryKey && rightAttribute != rightSchema.PrimaryKey) { Swap(ref leftFetch, ref rightFetch); Swap(ref leftEntity, ref rightEntity); Swap(ref leftAttribute, ref rightAttribute); Swap(ref leftAttributeParts, ref rightAttributeParts); Swap(ref leftSchema, ref rightSchema); } // Must be joining to the root entity of the right source, i.e. not a child link-entity if (!rightAttributeParts[0].Equals(rightFetch.Alias, StringComparison.OrdinalIgnoreCase)) { return(this); } // If there are any additional join criteria, either they must be able to be translated to FetchXml criteria // in the new link entity or we must be using an inner join so we can use a post-filter node var additionalCriteria = AdditionalJoinCriteria; var additionalLinkEntities = new Dictionary <object, List <FetchLinkEntityType> >(); if (TranslateFetchXMLCriteria(dataSource.Metadata, options, additionalCriteria, rightSchema, rightFetch.Alias, rightEntity.name, rightFetch.Alias, rightEntity.Items, out var filter, additionalLinkEntities)) { rightEntity.AddItem(filter); foreach (var kvp in additionalLinkEntities) { if (kvp.Key is FetchEntityType e) { foreach (var le in kvp.Value) { rightEntity.AddItem(le); } } else { foreach (var le in kvp.Value) { ((FetchLinkEntityType)kvp.Key).AddItem(le); } } } additionalCriteria = null; } if (additionalCriteria != null && JoinType != QualifiedJoinType.Inner) { return(this); } var rightLinkEntity = new FetchLinkEntityType { alias = rightFetch.Alias, name = rightEntity.name, linktype = JoinType == QualifiedJoinType.Inner ? "inner" : "outer", from = rightAttributeParts[1].ToLowerInvariant(), to = leftAttributeParts[1].ToLowerInvariant(), Items = rightEntity.Items }; // Find where the two FetchXml documents should be merged together and return the merged version if (leftAttributeParts[0].Equals(leftFetch.Alias)) { if (leftEntity.Items == null) { leftEntity.Items = new object[] { rightLinkEntity } } ; else { leftEntity.Items = leftEntity.Items.Concat(new object[] { rightLinkEntity }).ToArray(); } } else { var leftLinkEntity = leftFetch.Entity.FindLinkEntity(leftAttributeParts[0]); if (leftLinkEntity == null) { return(this); } if (leftLinkEntity.Items == null) { leftLinkEntity.Items = new object[] { rightLinkEntity } } ; else { leftLinkEntity.Items = leftLinkEntity.Items.Concat(new object[] { rightLinkEntity }).ToArray(); } } if (additionalCriteria != null) { return new FilterNode { Filter = additionalCriteria, Source = leftFetch } }