Exemple #1
0
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            Source = Source.FoldQuery(dataSources, options, parameterTypes, hints);

            // Combine multiple ComputeScalar nodes. Calculations in this node might be dependent on those in the previous node, so rewrite any references
            // to the earlier computed columns
            if (Source is ComputeScalarNode computeScalar)
            {
                var rewrites = new Dictionary <ScalarExpression, ScalarExpression>();

                foreach (var prevCalc in computeScalar.Columns)
                {
                    rewrites[prevCalc.Key.ToColumnReference()] = prevCalc.Value;
                }

                var rewrite = new RewriteVisitor(rewrites);

                foreach (var calc in Columns)
                {
                    computeScalar.Columns.Add(calc.Key, rewrite.ReplaceExpression(calc.Value));
                }

                return(computeScalar);
            }

            Source.Parent = this;
            return(this);
        }
Exemple #2
0
        internal static void ExpandWildcardColumns(IDataExecutionPlanNode source, List <SelectColumn> columnSet, IDictionary <string, DataSource> dataSources, IDictionary <string, Type> parameterTypes)
        {
            // Expand any AllColumns
            if (columnSet.Any(col => col.AllColumns))
            {
                var schema   = source.GetSchema(dataSources, parameterTypes);
                var expanded = new List <SelectColumn>();

                foreach (var col in columnSet)
                {
                    if (!col.AllColumns)
                    {
                        expanded.Add(col);
                        continue;
                    }

                    foreach (var src in schema.Schema.Keys.Where(k => col.SourceColumn == null || k.StartsWith(col.SourceColumn.Replace("*", ""), StringComparison.OrdinalIgnoreCase)).OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
                    {
                        expanded.Add(new SelectColumn
                        {
                            SourceColumn = src,
                            OutputColumn = src.Split('.').Last()
                        });
                    }
                }

                columnSet.Clear();
                columnSet.AddRange(expanded);
            }
        }
Exemple #3
0
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            Source        = Source.FoldQuery(dataSources, options, parameterTypes, hints);
            Source.Parent = this;

            if (!Offset.IsConstantValueExpression(null, options, out var offsetLiteral) ||
                !Fetch.IsConstantValueExpression(null, options, out var fetchLiteral))
            {
                return(this);
            }

            if (Source is FetchXmlScan fetchXml)
            {
                var offset = SqlTypeConverter.ChangeType <int>(offsetLiteral.Compile(null, null)(null, null, options));
                var count  = SqlTypeConverter.ChangeType <int>(fetchLiteral.Compile(null, null)(null, null, options));
                var page   = offset / count;

                if (page * count == offset && count <= 5000)
                {
                    fetchXml.FetchXml.count = count.ToString();
                    fetchXml.FetchXml.page  = (page + 1).ToString();
                    fetchXml.AllPages       = false;
                    return(fetchXml);
                }
            }

            return(this);
        }
Exemple #4
0
        private IEnumerable <IDataExecutionPlanNode> GetFoldableSources(IDataExecutionPlanNode source)
        {
            if (source is FetchXmlScan)
            {
                yield return(source);

                yield break;
            }

            if (source is MetadataQueryNode)
            {
                yield return(source);

                yield break;
            }

            if (source is BaseJoinNode join)
            {
                if (join.JoinType == QualifiedJoinType.Inner || join.JoinType == QualifiedJoinType.LeftOuter)
                {
                    foreach (var subSource in GetFoldableSources(join.LeftSource))
                    {
                        yield return(subSource);
                    }
                }

                if (join.JoinType == QualifiedJoinType.Inner || join.JoinType == QualifiedJoinType.RightOuter)
                {
                    foreach (var subSource in GetFoldableSources(join.RightSource))
                    {
                        yield return(subSource);
                    }
                }

                yield break;
            }

            if (source is HashMatchAggregateNode)
            {
                yield break;
            }

            if (source is TableSpoolNode)
            {
                yield break;
            }

            foreach (var subSource in source.GetSources().OfType <IDataExecutionPlanNode>())
            {
                foreach (var foldableSubSource in GetFoldableSources(subSource))
                {
                    yield return(foldableSubSource);
                }
            }
        }
Exemple #5
0
        private void FoldMetadataColumns(IDataExecutionPlanNode source, List <SelectColumn> columnSet)
        {
            if (source is MetadataQueryNode metadata)
            {
                // Check if there are any wildcard columns we can apply to the source metadata query
                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);

                if (metadata.MetadataSource.HasFlag(MetadataSource.Entity) && (hasStar || aliasStars.Contains(metadata.EntityAlias)))
                {
                    if (metadata.Query.Properties == null)
                    {
                        metadata.Query.Properties = new MetadataPropertiesExpression();
                    }

                    metadata.Query.Properties.AllProperties = true;
                }

                if (metadata.MetadataSource.HasFlag(MetadataSource.Attribute) && (hasStar || aliasStars.Contains(metadata.AttributeAlias)))
                {
                    if (metadata.Query.AttributeQuery == null)
                    {
                        metadata.Query.AttributeQuery = new AttributeQueryExpression();
                    }

                    if (metadata.Query.AttributeQuery.Properties == null)
                    {
                        metadata.Query.AttributeQuery.Properties = new MetadataPropertiesExpression();
                    }

                    metadata.Query.AttributeQuery.Properties.AllProperties = true;
                }

                if ((metadata.MetadataSource.HasFlag(MetadataSource.OneToManyRelationship) && (hasStar || aliasStars.Contains(metadata.OneToManyRelationshipAlias))) ||
                    (metadata.MetadataSource.HasFlag(MetadataSource.ManyToOneRelationship) && (hasStar || aliasStars.Contains(metadata.ManyToOneRelationshipAlias))) ||
                    (metadata.MetadataSource.HasFlag(MetadataSource.ManyToManyRelationship) && (hasStar || aliasStars.Contains(metadata.ManyToManyRelationshipAlias))))
                {
                    if (metadata.Query.RelationshipQuery == null)
                    {
                        metadata.Query.RelationshipQuery = new RelationshipQueryExpression();
                    }

                    if (metadata.Query.RelationshipQuery.Properties == null)
                    {
                        metadata.Query.RelationshipQuery.Properties = new MetadataPropertiesExpression();
                    }

                    metadata.Query.RelationshipQuery.Properties.AllProperties = true;
                }
            }
        }
Exemple #6
0
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            Source = Source.FoldQuery(dataSources, options, parameterTypes, hints);

            // These sorts will override any previous sort
            if (Source is SortNode prevSort)
            {
                Source = prevSort.Source;
            }

            Source.Parent = this;

            return(FoldSorts(dataSources, options, parameterTypes));
        }
Exemple #7
0
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            Source        = Source.FoldQuery(dataSources, options, parameterTypes, hints);
            Source.Parent = this;

            SelectNode.FoldFetchXmlColumns(Source, ColumnSet, dataSources, parameterTypes);
            SelectNode.ExpandWildcardColumns(Source, ColumnSet, dataSources, parameterTypes);

            if (Source is FetchXmlScan fetchXml)
            {
                // Check if all the source and output column names match. If so, just change the alias of the source FetchXML
                if (ColumnSet.All(col => col.SourceColumn == $"{fetchXml.Alias}.{col.OutputColumn}"))
                {
                    fetchXml.Alias = Alias;
                    return(fetchXml);
                }
            }

            return(this);
        }
Exemple #8
0
        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);
                        }
                    }
                }
            }
        }
Exemple #9
0
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            Source        = Source.FoldQuery(dataSources, options, parameterTypes, hints);
            Source.Parent = this;

            // Remove any duplicated column names
            for (var i = Columns.Count - 1; i >= 0; i--)
            {
                if (Columns.IndexOf(Columns[i]) < i)
                {
                    Columns.RemoveAt(i);
                }
            }

            // If one of the fields to include in the DISTINCT calculation is the primary key, there is no possibility of duplicate
            // rows so we can discard the distinct node
            var schema = Source.GetSchema(dataSources, parameterTypes);

            if (!String.IsNullOrEmpty(schema.PrimaryKey) && Columns.Contains(schema.PrimaryKey, StringComparer.OrdinalIgnoreCase))
            {
                return(Source);
            }

            if (Source is FetchXmlScan fetch)
            {
                fetch.FetchXml.distinct          = true;
                fetch.FetchXml.distinctSpecified = true;

                // Ensure there is a sort order applied to avoid paging issues
                if (fetch.Entity.Items == null || !fetch.Entity.Items.OfType <FetchOrderType>().Any())
                {
                    // Sort by each attribute. Make sure we only add one sort per attribute, taking virtual attributes
                    // into account (i.e. don't attempt to sort on statecode and statecodename)
                    var sortedAttributes = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

                    foreach (var column in Columns)
                    {
                        if (!schema.ContainsColumn(column, out var normalized))
                        {
                            continue;
                        }

                        if (!sortedAttributes.Add(normalized))
                        {
                            continue;
                        }

                        var parts = normalized.Split('.');
                        if (parts.Length != 2)
                        {
                            continue;
                        }

                        if (parts[0].Equals(fetch.Alias, StringComparison.OrdinalIgnoreCase))
                        {
                            var attr = dataSources[fetch.DataSource].Metadata[fetch.Entity.name].Attributes.SingleOrDefault(a => a.LogicalName.Equals(parts[1], StringComparison.OrdinalIgnoreCase));

                            if (attr == null)
                            {
                                continue;
                            }

                            if (attr.AttributeOf != null && !sortedAttributes.Add(parts[0] + "." + attr.AttributeOf))
                            {
                                continue;
                            }

                            fetch.Entity.AddItem(new FetchOrderType {
                                attribute = parts[1]
                            });
                        }
                        else
                        {
                            var linkEntity = fetch.Entity.FindLinkEntity(parts[0]);
                            var attr       = dataSources[fetch.DataSource].Metadata[linkEntity.name].Attributes.SingleOrDefault(a => a.LogicalName.Equals(parts[1], StringComparison.OrdinalIgnoreCase));

                            if (attr == null)
                            {
                                continue;
                            }

                            if (attr.AttributeOf != null && !sortedAttributes.Add(parts[0] + "." + attr.AttributeOf))
                            {
                                continue;
                            }

                            linkEntity.AddItem(new FetchOrderType {
                                attribute = parts[1]
                            });
                        }
                    }
                }

                return(fetch);
            }

            // If the data is already sorted by all the distinct columns we can use a stream aggregate instead.
            // We don't mind what order the columns are sorted in though, so long as the distinct columns form a
            // prefix of the sort order.
            var requiredSorts = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            foreach (var col in Columns)
            {
                if (!schema.ContainsColumn(col, out var column))
                {
                    return(this);
                }

                requiredSorts.Add(column);
            }

            if (!schema.IsSortedBy(requiredSorts))
            {
                return(this);
            }

            var aggregate = new StreamAggregateNode {
                Source = Source
            };

            Source.Parent = aggregate;

            for (var i = 0; i < requiredSorts.Count; i++)
            {
                aggregate.GroupBy.Add(schema.SortOrder[i].ToColumnReference());
            }

            return(aggregate);
        }
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            if (_folded)
            {
                return(this);
            }

            Source        = Source.FoldQuery(dataSources, options, parameterTypes, hints);
            Source.Parent = this;

            // Special case for using RetrieveTotalRecordCount instead of FetchXML
            if (options.UseRetrieveTotalRecordCount &&
                Source is FetchXmlScan fetch &&
                (fetch.Entity.Items == null || fetch.Entity.Items.Length == 0) &&
                GroupBy.Count == 0 &&
                Aggregates.Count == 1 &&
                Aggregates.Single().Value.AggregateType == AggregateType.CountStar &&
                dataSources[fetch.DataSource].Metadata[fetch.Entity.name].DataProviderId == null) // RetrieveTotalRecordCountRequest is not valid for virtual entities
            {
                var count = new RetrieveTotalRecordCountNode {
                    DataSource = fetch.DataSource, EntityName = fetch.Entity.name
                };
                var countName = count.GetSchema(dataSources, parameterTypes).Schema.Single().Key;

                if (countName == Aggregates.Single().Key)
                {
                    return(count);
                }

                var rename = new ComputeScalarNode
                {
                    Source  = count,
                    Columns =
                    {
                        [Aggregates.Single().Key] = new ColumnReferenceExpression
                            {
                            MultiPartIdentifier = new MultiPartIdentifier
                            {
                            Identifiers = { new Identifier {
                                                Value = countName
                                            } }
                            }
                            }
                    }
                };
                count.Parent = rename;

                return(rename);
            }

            if (Source is FetchXmlScan || Source is ComputeScalarNode computeScalar && computeScalar.Source is FetchXmlScan)
            {
                // Check if all the aggregates & groupings can be done in FetchXML. Can only convert them if they can ALL
                // be handled - if any one needs to be calculated manually, we need to calculate them all.
                var canUseFetchXmlAggregate = true;

                // Also track if we can partition the query for larger source data sets. We can't partition DISTINCT aggregates,
                // and need to transform AVG(field) to SUM(field) / COUNT(field)
                var canPartition = true;

                foreach (var agg in Aggregates)
                {
                    if (agg.Value.SqlExpression != null && !(agg.Value.SqlExpression is ColumnReferenceExpression))
                    {
                        canUseFetchXmlAggregate = false;
                        break;
                    }

                    if (agg.Value.Distinct && agg.Value.AggregateType != AggregateType.Count)
                    {
                        canUseFetchXmlAggregate = false;
                        break;
                    }

                    if (agg.Value.AggregateType == AggregateType.First)
                    {
                        canUseFetchXmlAggregate = false;
                        break;
                    }

                    if (agg.Value.Distinct)
                    {
                        canPartition = false;
                    }
                }

                var fetchXml = Source as FetchXmlScan;
                computeScalar = Source as ComputeScalarNode;

                var partnames = new Dictionary <string, FetchXml.DateGroupingType>(StringComparer.OrdinalIgnoreCase)
                {
                    ["year"]    = DateGroupingType.year,
                    ["yy"]      = DateGroupingType.year,
                    ["yyyy"]    = DateGroupingType.year,
                    ["quarter"] = DateGroupingType.quarter,
                    ["qq"]      = DateGroupingType.quarter,
                    ["q"]       = DateGroupingType.quarter,
                    ["month"]   = DateGroupingType.month,
                    ["mm"]      = DateGroupingType.month,
                    ["m"]       = DateGroupingType.month,
                    ["day"]     = DateGroupingType.day,
                    ["dd"]      = DateGroupingType.day,
                    ["d"]       = DateGroupingType.day,
                    ["week"]    = DateGroupingType.week,
                    ["wk"]      = DateGroupingType.week,
                    ["ww"]      = DateGroupingType.week
                };

                if (computeScalar != null)
                {
                    fetchXml = (FetchXmlScan)computeScalar.Source;

                    // Groupings may be on DATEPART function, which will have been split into separate Compute Scalar node. Check if all the scalar values
                    // being computed are DATEPART functions that can be converted to FetchXML and are used as groupings
                    foreach (var scalar in computeScalar.Columns)
                    {
                        if (!(scalar.Value is FunctionCall func) ||
                            !func.FunctionName.Value.Equals("DATEPART", StringComparison.OrdinalIgnoreCase) ||
                            func.Parameters.Count != 2 ||
                            !(func.Parameters[0] is ColumnReferenceExpression datePartType) ||
                            !(func.Parameters[1] is ColumnReferenceExpression datePartCol))
                        {
                            canUseFetchXmlAggregate = false;
                            break;
                        }

                        if (!GroupBy.Any(g => g.MultiPartIdentifier.Identifiers.Count == 1 && g.MultiPartIdentifier.Identifiers[0].Value == scalar.Key))
                        {
                            canUseFetchXmlAggregate = false;
                            break;
                        }

                        if (!partnames.ContainsKey(datePartType.GetColumnName()))
                        {
                            canUseFetchXmlAggregate = false;
                            break;
                        }

                        // FetchXML dategrouping always uses local timezone. If we're using UTC we can't use it
                        if (!options.UseLocalTimeZone)
                        {
                            canUseFetchXmlAggregate = false;
                            break;
                        }
                    }
                }

                var metadata = dataSources[fetchXml.DataSource].Metadata;

                // FetchXML is translated to QueryExpression for virtual entities, which doesn't support aggregates
                if (metadata[fetchXml.Entity.name].DataProviderId != null)
                {
                    canUseFetchXmlAggregate = false;
                }

                // Check FetchXML supports grouping by each of the requested attributes
                var fetchSchema = fetchXml.GetSchema(dataSources, parameterTypes);
                foreach (var group in GroupBy)
                {
                    if (!fetchSchema.ContainsColumn(group.GetColumnName(), out var groupCol))
                    {
                        continue;
                    }

                    var    parts = groupCol.Split('.');
                    string entityName;

                    if (parts[0] == fetchXml.Alias)
                    {
                        entityName = fetchXml.Entity.name;
                    }
                    else
                    {
                        entityName = fetchXml.Entity.FindLinkEntity(parts[0]).name;
                    }

                    var attr = metadata[entityName].Attributes.SingleOrDefault(a => a.LogicalName == parts[1]);

                    // Can't group by virtual attributes
                    if (attr == null || attr.AttributeOf != null)
                    {
                        canUseFetchXmlAggregate = false;
                    }

                    // Can't group by multi-select picklist attributes
                    if (attr is MultiSelectPicklistAttributeMetadata)
                    {
                        canUseFetchXmlAggregate = false;
                    }
                }

                var serializer = new XmlSerializer(typeof(FetchXml.FetchType));

                if (canUseFetchXmlAggregate)
                {
                    // FetchXML aggregates can trigger an AggregateQueryRecordLimitExceeded error. Clone the non-aggregate FetchXML
                    // so we can try to run the native aggregate version but fall back to in-memory processing where necessary
                    var clonedFetchXml = new FetchXmlScan
                    {
                        DataSource       = fetchXml.DataSource,
                        Alias            = fetchXml.Alias,
                        AllPages         = fetchXml.AllPages,
                        FetchXml         = (FetchXml.FetchType)serializer.Deserialize(new StringReader(fetchXml.FetchXmlString)),
                        ReturnFullSchema = fetchXml.ReturnFullSchema
                    };

                    if (Source == fetchXml)
                    {
                        Source = clonedFetchXml;
                        clonedFetchXml.Parent = this;
                    }
                    else
                    {
                        computeScalar.Source  = clonedFetchXml;
                        clonedFetchXml.Parent = computeScalar;
                    }

                    fetchXml.FetchXml.aggregate          = true;
                    fetchXml.FetchXml.aggregateSpecified = true;
                    fetchXml.FetchXml = fetchXml.FetchXml;

                    var schema = Source.GetSchema(dataSources, parameterTypes);

                    foreach (var grouping in GroupBy)
                    {
                        var colName = grouping.GetColumnName();
                        var alias   = grouping.MultiPartIdentifier.Identifiers.Last().Value;
                        DateGroupingType?dateGrouping = null;

                        if (computeScalar != null && computeScalar.Columns.TryGetValue(colName, out var datePart))
                        {
                            dateGrouping = partnames[((ColumnReferenceExpression)((FunctionCall)datePart).Parameters[0]).GetColumnName()];
                            colName      = ((ColumnReferenceExpression)((FunctionCall)datePart).Parameters[1]).GetColumnName();
                        }

                        schema.ContainsColumn(colName, out colName);

                        var attribute = fetchXml.AddAttribute(colName, a => a.groupbySpecified && a.groupby == FetchBoolType.@true && a.alias == alias, metadata, out _, out var linkEntity);
                        attribute.groupby          = FetchBoolType.@true;
                        attribute.groupbySpecified = true;
                        attribute.alias            = alias;

                        if (dateGrouping != null)
                        {
                            attribute.dategrouping          = dateGrouping.Value;
                            attribute.dategroupingSpecified = true;
                        }
                        else if (grouping.GetType(schema, null, parameterTypes) == typeof(SqlDateTime))
                        {
                            // Can't group on datetime columns without a DATEPART specification
                            canUseFetchXmlAggregate = false;
                        }

                        // Add a sort order for each grouping to allow consistent paging
                        var items = linkEntity?.Items ?? fetchXml.Entity.Items;
                        var sort  = items.OfType <FetchOrderType>().FirstOrDefault(order => order.alias == alias);
                        if (sort == null)
                        {
                            if (linkEntity == null)
                            {
                                fetchXml.Entity.AddItem(new FetchOrderType {
                                    alias = alias
                                });
                            }
                            else
                            {
                                linkEntity.AddItem(new FetchOrderType {
                                    alias = alias
                                });
                            }
                        }
                    }

                    foreach (var agg in Aggregates)
                    {
                        var col     = (ColumnReferenceExpression)agg.Value.SqlExpression;
                        var colName = col == null ? (fetchXml.Alias + "." + metadata[fetchXml.Entity.name].PrimaryIdAttribute) : col.GetColumnName();

                        if (!schema.ContainsColumn(colName, out colName))
                        {
                            canUseFetchXmlAggregate = false;
                        }

                        var distinct = agg.Value.Distinct ? FetchBoolType.@true : FetchBoolType.@false;

                        FetchXml.AggregateType aggregateType;

                        switch (agg.Value.AggregateType)
                        {
                        case AggregateType.Average:
                            aggregateType = FetchXml.AggregateType.avg;
                            break;

                        case AggregateType.Count:
                            aggregateType = FetchXml.AggregateType.countcolumn;
                            break;

                        case AggregateType.CountStar:
                            aggregateType = FetchXml.AggregateType.count;
                            break;

                        case AggregateType.Max:
                            aggregateType = FetchXml.AggregateType.max;
                            break;

                        case AggregateType.Min:
                            aggregateType = FetchXml.AggregateType.min;
                            break;

                        case AggregateType.Sum:
                            aggregateType = FetchXml.AggregateType.sum;
                            break;

                        default:
                            throw new ArgumentOutOfRangeException();
                        }

                        // min, max, sum and avg are not supported for optionset attributes
                        var    parts = colName.Split('.');
                        string entityName;

                        if (parts[0] == fetchXml.Alias)
                        {
                            entityName = fetchXml.Entity.name;
                        }
                        else
                        {
                            entityName = fetchXml.Entity.FindLinkEntity(parts[0]).name;
                        }

                        var attr = metadata[entityName].Attributes.SingleOrDefault(a => a.LogicalName == parts[1]);

                        if (attr == null)
                        {
                            canUseFetchXmlAggregate = false;
                        }

                        if (attr is EnumAttributeMetadata && (aggregateType == FetchXml.AggregateType.avg || aggregateType == FetchXml.AggregateType.max || aggregateType == FetchXml.AggregateType.min || aggregateType == FetchXml.AggregateType.sum))
                        {
                            canUseFetchXmlAggregate = false;
                        }

                        var attribute = fetchXml.AddAttribute(colName, a => a.aggregate == aggregateType && a.alias == agg.Key && a.distinct == distinct, metadata, out _, out _);
                        attribute.aggregate          = aggregateType;
                        attribute.aggregateSpecified = true;
                        attribute.alias = agg.Key;

                        if (agg.Value.Distinct)
                        {
                            attribute.distinct          = distinct;
                            attribute.distinctSpecified = true;
                        }
                    }
                }

                // FoldQuery can be called again in some circumstances. Don't repeat the folding operation and create another try/catch
                _folded = true;

                // Check how we should execute this aggregate if the FetchXML aggregate fails or is not available. Use stream aggregate
                // for scalar aggregates or where all the grouping fields can be folded into sorts.
                var nonFetchXmlAggregate = FoldToStreamAggregate(dataSources, options, parameterTypes, hints);

                if (!canUseFetchXmlAggregate)
                {
                    return(nonFetchXmlAggregate);
                }

                IDataExecutionPlanNode firstTry = fetchXml;

                // If the main aggregate query fails due to having over 50K records, check if we can retry with partitioning. We
                // need a createdon field to be available for this to work.
                if (canPartition)
                {
                    canPartition = metadata[fetchXml.Entity.name].Attributes.Any(a => a.LogicalName == "createdon");
                }

                if (canUseFetchXmlAggregate && canPartition)
                {
                    // Create a clone of the aggregate FetchXML query
                    var partitionedFetchXml = new FetchXmlScan
                    {
                        DataSource       = fetchXml.DataSource,
                        Alias            = fetchXml.Alias,
                        AllPages         = fetchXml.AllPages,
                        FetchXml         = (FetchXml.FetchType)serializer.Deserialize(new StringReader(fetchXml.FetchXmlString)),
                        ReturnFullSchema = fetchXml.ReturnFullSchema
                    };

                    var partitionedAggregates = new PartitionedAggregateNode
                    {
                        Source = partitionedFetchXml
                    };
                    partitionedFetchXml.Parent = partitionedAggregates;
                    var partitionedResults = (IDataExecutionPlanNode)partitionedAggregates;

                    partitionedAggregates.GroupBy.AddRange(GroupBy);

                    foreach (var aggregate in Aggregates)
                    {
                        if (aggregate.Value.AggregateType != AggregateType.Average)
                        {
                            partitionedAggregates.Aggregates[aggregate.Key] = aggregate.Value;
                        }
                        else
                        {
                            // Rewrite AVG as SUM / COUNT
                            partitionedAggregates.Aggregates[aggregate.Key + "_sum"] = new Aggregate
                            {
                                AggregateType = AggregateType.Sum,
                                SqlExpression = aggregate.Value.SqlExpression
                            };
                            partitionedAggregates.Aggregates[aggregate.Key + "_count"] = new Aggregate
                            {
                                AggregateType = AggregateType.Count,
                                SqlExpression = aggregate.Value.SqlExpression
                            };

                            if (partitionedResults == partitionedAggregates)
                            {
                                partitionedResults = new ComputeScalarNode {
                                    Source = partitionedAggregates
                                };
                                partitionedAggregates.Parent = partitionedResults;
                            }

                            // Handle count = 0 => null
                            ((ComputeScalarNode)partitionedResults).Columns[aggregate.Key] = new SearchedCaseExpression
                            {
                                WhenClauses =
                                {
                                    new SearchedWhenClause
                                    {
                                        WhenExpression = new BooleanComparisonExpression
                                        {
                                            FirstExpression  = (aggregate.Key + "_count").ToColumnReference(),
                                            ComparisonType   = BooleanComparisonType.Equals,
                                            SecondExpression = new IntegerLiteral{
                                                Value = "0"
                                            }
                                        },
                                        ThenExpression = new NullLiteral()
                                    }
                                },
                                ElseExpression = new BinaryExpression
                                {
                                    FirstExpression      = (aggregate.Key + "_sum").ToColumnReference(),
                                    BinaryExpressionType = BinaryExpressionType.Divide,
                                    SecondExpression     = (aggregate.Key + "_count").ToColumnReference()
                                }
                            };

                            // Find the AVG expression in the FetchXML and replace with _sum and _count
                            var avg      = partitionedFetchXml.Entity.FindAliasedAttribute(aggregate.Key, null, out var linkEntity);
                            var sumCount = new object[]
                            {
                                new FetchAttributeType
                                {
                                    name  = avg.name,
                                    alias = avg.alias + "_sum",
                                    aggregateSpecified = true,
                                    aggregate          = FetchXml.AggregateType.sum
                                },
                                new FetchAttributeType
                                {
                                    name  = avg.name,
                                    alias = avg.alias + "_count",
                                    aggregateSpecified = true,
                                    aggregate          = FetchXml.AggregateType.countcolumn
                                }
                            };

                            if (linkEntity == null)
                            {
                                partitionedFetchXml.Entity.Items = partitionedFetchXml.Entity.Items
                                                                   .Except(new[] { avg })
                                                                   .Concat(sumCount)
                                                                   .ToArray();
                            }
                            else
                            {
                                linkEntity.Items = linkEntity.Items
                                                   .Except(new[] { avg })
                                                   .Concat(sumCount)
                                                   .ToArray();
                            }
                        }
                    }

                    var tryPartitioned = new TryCatchNode
                    {
                        TrySource       = firstTry,
                        CatchSource     = partitionedResults,
                        ExceptionFilter = ex => GetOrganizationServiceFault(ex, out var fault) && IsAggregateQueryLimitExceeded(fault)
                    };
                    partitionedResults.Parent = tryPartitioned;
                    firstTry.Parent           = tryPartitioned;
                    firstTry = tryPartitioned;
                }

                var tryCatch = new TryCatchNode
                {
                    TrySource       = firstTry,
                    CatchSource     = nonFetchXmlAggregate,
                    ExceptionFilter = ex => (ex is QueryExecutionException qee && (qee.InnerException is PartitionedAggregateNode.PartitionOverflowException || qee.InnerException is FetchXmlScan.InvalidPagingException)) || (GetOrganizationServiceFault(ex, out var fault) && IsAggregateQueryRetryable(fault))
                };

                firstTry.Parent             = tryCatch;
                nonFetchXmlAggregate.Parent = tryCatch;
                return(tryCatch);
            }

            return(FoldToStreamAggregate(dataSources, options, parameterTypes, hints));
        }
Exemple #11
0
        public override IDataExecutionPlanNode FoldQuery(IDictionary <string, DataSource> dataSources, IQueryExecutionOptions options, IDictionary <string, Type> parameterTypes, IList <OptimizerHint> hints)
        {
            Source        = Source.FoldQuery(dataSources, options, parameterTypes, hints);
            Source.Parent = this;

            // Foldable correlated IN queries "lefttable.column IN (SELECT righttable.column FROM righttable WHERE ...) are created as:
            // Filter: Expr2 is not null
            // -> FoldableJoin (LeftOuter SemiJoin) Expr2 = righttable.column in DefinedValues; righttable.column in RightAttribute
            //    -> FetchXml
            //    -> FetchXml (Distinct) orderby righttable.column

            // Foldable correlated EXISTS filters "EXISTS (SELECT * FROM righttable WHERE righttable.column = lefttable.column AND ...) are created as:
            // Filter - @var2 is not null
            // -> NestedLoop(LeftOuter SemiJoin), null join condition. Outer reference(lefttable.column -> @var1), Defined values(@var2 -> rightttable.primarykey)
            //   -> FetchXml
            //   -> Top 1
            //      -> Index spool, SeekValue @var1, KeyColumn rightttable.column
            //         -> FetchXml
            var joins = new List <BaseJoinNode>();
            var join  = Source as BaseJoinNode;

            while (join != null)
            {
                joins.Add(join);

                if (join is MergeJoinNode && join.LeftSource is SortNode sort)
                {
                    join = sort.Source as BaseJoinNode;
                }
                else
                {
                    join = join.LeftSource as BaseJoinNode;
                }
            }

            var          addedLinks = new List <FetchLinkEntityType>();
            FetchXmlScan leftFetch;

            if (joins.Count == 0)
            {
                leftFetch = null;
            }
            else
            {
                var lastJoin = joins.Last();
                if (lastJoin is MergeJoinNode && lastJoin.LeftSource is SortNode sort)
                {
                    leftFetch = sort.Source as FetchXmlScan;
                }
                else
                {
                    leftFetch = lastJoin.LeftSource as FetchXmlScan;
                }
            }

            while (leftFetch != null && joins.Count > 0)
            {
                join = joins.Last();

                if (join.JoinType != QualifiedJoinType.LeftOuter ||
                    !join.SemiJoin)
                {
                    break;
                }

                FetchLinkEntityType linkToAdd;
                string leftAlias;

                if (join is FoldableJoinNode merge)
                {
                    // Check we meet all the criteria for a foldable correlated IN query
                    var rightSort  = join.RightSource as SortNode;
                    var rightFetch = (rightSort?.Source ?? join.RightSource) as FetchXmlScan;

                    if (rightFetch == null)
                    {
                        break;
                    }

                    if (!leftFetch.DataSource.Equals(rightFetch.DataSource, StringComparison.OrdinalIgnoreCase))
                    {
                        break;
                    }

                    // Sorts could be folded into FetchXML or could be in separate node
                    string attribute;

                    if (rightSort != null)
                    {
                        if (rightSort.Sorts.Count != 1)
                        {
                            break;
                        }

                        if (!(rightSort.Sorts[0].Expression is ColumnReferenceExpression sortCol))
                        {
                            break;
                        }

                        attribute = sortCol.GetColumnName();
                    }
                    else
                    {
                        var rightSorts = (rightFetch.Entity.Items ?? Array.Empty <object>()).OfType <FetchOrderType>().ToList();

                        if (rightSorts.Count != 1)
                        {
                            break;
                        }

                        if (!String.IsNullOrEmpty(rightSorts[0].alias))
                        {
                            break;
                        }

                        attribute = $"{rightFetch.Alias}.{rightSorts[0].attribute}";
                    }

                    if (!merge.RightAttribute.GetColumnName().Equals(attribute, StringComparison.OrdinalIgnoreCase))
                    {
                        break;
                    }

                    var rightSchema = rightFetch.GetSchema(dataSources, parameterTypes);

                    // Right values need to be distinct - still allowed if it's the primary key
                    if (!rightFetch.FetchXml.distinct && rightSchema.PrimaryKey != attribute)
                    {
                        break;
                    }

                    var definedValueName = join.DefinedValues.SingleOrDefault(kvp => kvp.Value == attribute).Key;

                    if (definedValueName == null)
                    {
                        break;
                    }

                    var notNullFilter = FindNotNullFilter(Filter, definedValueName);
                    if (notNullFilter == null)
                    {
                        break;
                    }

                    // We can fold IN to a simple left outer join where the attribute is the primary key
                    if (!rightFetch.FetchXml.distinct && rightSchema.PrimaryKey == attribute)
                    {
                        // Replace the filter on the defined value name with a filter on the primary key column
                        notNullFilter.Expression = attribute.ToColumnReference();

                        linkToAdd = new FetchLinkEntityType
                        {
                            name     = rightFetch.Entity.name,
                            alias    = rightFetch.Alias,
                            from     = merge.RightAttribute.MultiPartIdentifier.Identifiers.Last().Value,
                            to       = merge.LeftAttribute.MultiPartIdentifier.Identifiers.Last().Value,
                            linktype = "outer",
                            Items    = rightFetch.Entity.Items.Where(i => !(i is FetchOrderType)).ToArray()
                        };
                    }
                    else
                    {
                        // We need to use an "in" join type - check that's supported
                        if (!options.JoinOperatorsAvailable.Contains(JoinOperator.Any))
                        {
                            break;
                        }

                        // Remove the filter and replace with an "in" link-entity
                        Filter = Filter.RemoveCondition(notNullFilter);

                        linkToAdd = new FetchLinkEntityType
                        {
                            name     = rightFetch.Entity.name,
                            alias    = rightFetch.Alias,
                            from     = merge.RightAttribute.MultiPartIdentifier.Identifiers.Last().Value,
                            to       = merge.LeftAttribute.MultiPartIdentifier.Identifiers.Last().Value,
                            linktype = "in",
                            Items    = rightFetch.Entity.Items.Where(i => !(i is FetchOrderType)).ToArray()
                        };
                    }

                    leftAlias = merge.LeftAttribute.MultiPartIdentifier.Identifiers.Reverse().Skip(1).First().Value;

                    // Remove the sort that has been merged into the left side too
                    if (leftFetch.Entity.Items != null)
                    {
                        leftFetch.Entity.Items = leftFetch.Entity
                                                 .Items
                                                 .Where(i => !(i is FetchOrderType sort) || !sort.attribute.Equals(merge.LeftAttribute.MultiPartIdentifier.Identifiers.Last().Value, StringComparison.OrdinalIgnoreCase))
                                                 .ToArray();
                    }
                }
                else if (join is NestedLoopNode loop)
                {
                    // Check we meet all the criteria for a foldable correlated IN query
                    if (!options.JoinOperatorsAvailable.Contains(JoinOperator.Exists))
                    {
                        break;
                    }

                    if (loop.JoinCondition != null ||
                        loop.OuterReferences.Count != 1 ||
                        loop.DefinedValues.Count != 1)
                    {
                        break;
                    }

                    if (!(join.RightSource is TopNode top))
                    {
                        break;
                    }

                    if (!(top.Top is IntegerLiteral topLiteral) ||
                        topLiteral.Value != "1")
                    {
                        break;
                    }

                    if (!(top.Source is IndexSpoolNode indexSpool))
                    {
                        break;
                    }

                    if (indexSpool.SeekValue != loop.OuterReferences.Single().Value)
                    {
                        break;
                    }

                    if (!(indexSpool.Source is FetchXmlScan rightFetch))
                    {
                        break;
                    }

                    if (indexSpool.KeyColumn.Split('.').Length != 2 ||
                        !indexSpool.KeyColumn.Split('.')[0].Equals(rightFetch.Alias, StringComparison.OrdinalIgnoreCase))
                    {
                        break;
                    }

                    var notNullFilter = FindNotNullFilter(Filter, loop.DefinedValues.Single().Key);
                    if (notNullFilter == null)
                    {
                        break;
                    }

                    // Remove the filter and replace with an "exists" link-entity
                    Filter = Filter.RemoveCondition(notNullFilter);

                    linkToAdd = new FetchLinkEntityType
                    {
                        name     = rightFetch.Entity.name,
                        alias    = rightFetch.Alias,
                        from     = indexSpool.KeyColumn.Split('.')[1],
                        to       = loop.OuterReferences.Single().Key.Split('.')[1],
                        linktype = "exists",
                        Items    = rightFetch.Entity.Items
                    };
                    leftAlias = loop.OuterReferences.Single().Key.Split('.')[0];
                }
                else
                {
                    // This isn't a type of join we can fold as a correlated IN/EXISTS join
                    break;
                }

                // Remove any attributes from the new linkentity
                var tempEntity = new FetchEntityType {
                    Items = new object[] { linkToAdd }
                };

                foreach (var link in tempEntity.GetLinkEntities())
                {
                    link.Items = (link.Items ?? Array.Empty <object>()).Where(i => !(i is FetchAttributeType) && !(i is allattributes)).ToArray();
                }

                if (leftAlias.Equals(leftFetch.Alias, StringComparison.OrdinalIgnoreCase))
                {
                    leftFetch.Entity.AddItem(linkToAdd);
                }
                else
                {
                    leftFetch.Entity.FindLinkEntity(leftAlias).AddItem(linkToAdd);
                }

                addedLinks.Add(linkToAdd);

                joins.Remove(join);

                if (joins.Count == 0)
                {
                    Source           = leftFetch;
                    leftFetch.Parent = this;
                }
                else
                {
                    join = joins.Last();

                    if (join is MergeJoinNode && join.LeftSource is SortNode sort)
                    {
                        sort.Source      = leftFetch;
                        leftFetch.Parent = sort;
                    }
                    else
                    {
                        join.LeftSource  = leftFetch;
                        leftFetch.Parent = join;
                    }
                }
            }

            // If we've got a filter matching a column and a variable (key lookup in a nested loop) from a table spool, replace it with a index spool
            if (Source is TableSpoolNode tableSpool)
            {
                var schema = Source.GetSchema(dataSources, parameterTypes);

                if (ExtractKeyLookupFilter(Filter, out var filter, out var indexColumn, out var seekVariable) && schema.ContainsColumn(indexColumn, out indexColumn))
                {
                    var spoolSource = tableSpool.Source;

                    // Index spool requires non-null key values
                    if (indexColumn != schema.PrimaryKey)
                    {
                        spoolSource = new FilterNode
                        {
                            Source = tableSpool.Source,
                            Filter = new BooleanIsNullExpression
                            {
                                Expression = indexColumn.ToColumnReference(),
                                IsNot      = true
                            }
                        }.FoldQuery(dataSources, options, parameterTypes, hints);
                    }

                    Source = new IndexSpoolNode
                    {
                        Source    = spoolSource,
                        KeyColumn = indexColumn,
                        SeekValue = seekVariable
                    };

                    Filter = filter;
                }
            }

            // Find all the data source nodes we could fold this into. Include direct data sources, those from either side of an inner join, or the main side of an outer join
            foreach (var source in GetFoldableSources(Source))
            {
                var schema = source.GetSchema(dataSources, parameterTypes);

                if (source is FetchXmlScan fetchXml && !fetchXml.FetchXml.aggregate)
                {
                    if (!dataSources.TryGetValue(fetchXml.DataSource, out var dataSource))
                    {
                        throw new NotSupportedQueryFragmentException("Missing datasource " + fetchXml.DataSource);
                    }

                    var additionalLinkEntities = new Dictionary <object, List <FetchLinkEntityType> >();

                    // If the criteria are ANDed, see if any of the individual conditions can be translated to FetchXML
                    Filter = ExtractFetchXMLFilters(dataSource.Metadata, options, Filter, schema, null, fetchXml.Entity.name, fetchXml.Alias, fetchXml.Entity.Items, out var fetchFilter, additionalLinkEntities);

                    if (fetchFilter != null)
                    {
                        fetchXml.Entity.AddItem(fetchFilter);

                        foreach (var kvp in additionalLinkEntities)
                        {
                            if (kvp.Key is FetchEntityType e)
                            {
                                foreach (var le in kvp.Value)
                                {
                                    fetchXml.Entity.AddItem(le);
                                }
                            }
                            else
                            {
                                foreach (var le in kvp.Value)
                                {
                                    ((FetchLinkEntityType)kvp.Key).AddItem(le);
                                }
                            }
                        }
                    }
                }

                if (source is MetadataQueryNode meta)
                {
                    // If the criteria are ANDed, see if any of the individual conditions can be translated to the metadata query
                    Filter = ExtractMetadataFilters(Filter, meta, options, out var entityFilter, out var attributeFilter, out var relationshipFilter);

                    meta.Query.AddFilter(entityFilter);

                    if (attributeFilter != null && meta.Query.AttributeQuery == null)
                    {
                        meta.Query.AttributeQuery = new AttributeQueryExpression();
                    }

                    meta.Query.AttributeQuery.AddFilter(attributeFilter);

                    if (relationshipFilter != null && meta.Query.RelationshipQuery == null)
                    {
                        meta.Query.RelationshipQuery = new RelationshipQueryExpression();
                    }

                    meta.Query.RelationshipQuery.AddFilter(relationshipFilter);
                }
            }

            foreach (var addedLink in addedLinks)
            {
                addedLink.SemiJoin = true;
            }

            if (Filter == null)
            {
                return(Source);
            }

            return(this);
        }