diff --git a/src/MongoDB.Driver/Core/Misc/Feature.cs b/src/MongoDB.Driver/Core/Misc/Feature.cs index 47bf7bb65f9..d2a3d86e88a 100644 --- a/src/MongoDB.Driver/Core/Misc/Feature.cs +++ b/src/MongoDB.Driver/Core/Misc/Feature.cs @@ -83,6 +83,8 @@ public class Feature private static readonly Feature __loookupConciseSyntax = new Feature("LoookupConciseSyntax", WireVersion.Server50); private static readonly Feature __loookupDocuments= new Feature("LoookupDocuments", WireVersion.Server60); private static readonly Feature __mmapV1StorageEngine = new Feature("MmapV1StorageEngine", WireVersion.Zero, WireVersion.Server42); + private static readonly Feature __medianOperator = new Feature("MedianOperator", WireVersion.Server70); + private static readonly Feature __percentileOperator = new Feature("PercentileOperator", WireVersion.Server70); private static readonly Feature __pickAccumulatorsNewIn52 = new Feature("PickAccumulatorsNewIn52", WireVersion.Server52); private static readonly Feature __rankFusionStage = new Feature("RankFusionStage", WireVersion.Server81); private static readonly Feature __regexMatch = new Feature("RegexMatch", WireVersion.Server42); @@ -401,6 +403,16 @@ public class Feature [Obsolete("This feature was removed in server version 4.2. As such, this property will be removed in a later release.")] public static Feature MmapV1StorageEngine => __mmapV1StorageEngine; + /// + /// Gets the $median operator added in 7.0 + /// + public static Feature MedianOperator => __medianOperator; + + /// + /// Gets the $percentile operator added in 7.0 + /// + public static Feature PercentileOperator => __percentileOperator; + /// /// Gets the pick accumulators new in 5.2 feature. /// diff --git a/src/MongoDB.Driver/Linq/ISetWindowFieldsPartitionExtensions.cs b/src/MongoDB.Driver/Linq/ISetWindowFieldsPartitionExtensions.cs index ca8aade374b..ecba3d5e81e 100644 --- a/src/MongoDB.Driver/Linq/ISetWindowFieldsPartitionExtensions.cs +++ b/src/MongoDB.Driver/Linq/ISetWindowFieldsPartitionExtensions.cs @@ -16,7 +16,6 @@ using System; using System.Collections.Generic; -using MongoDB.Bson; namespace MongoDB.Driver.Linq { @@ -879,6 +878,136 @@ public static TValue Max(this ISetWindowFieldsPartition throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); } + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static decimal Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static decimal? Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static double Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static double? Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static float Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static float? Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static double Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static double? Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static double Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the median of the numeric values. Median ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The window boundaries. + /// The median of the selected values. + public static double? Median(this ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + /// /// Returns the minimum value. /// @@ -893,6 +1022,146 @@ public static TValue Min(this ISetWindowFieldsPartition throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); } + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static decimal[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static decimal?[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static double[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static double?[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static float[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static float?[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static double[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static double?[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static double[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + + /// + /// Returns the values at the given percentiles. Percentile ignores non-numeric values. Percentile returns results in the same order as the given percentiles. + /// + /// The type of the input documents in the partition. + /// The partition. + /// The selector that selects a value from the input document. + /// The percentiles (between 0.0 and 1.0). + /// The window boundaries. + /// The values at the given percentiles. + public static double?[] Percentile(this ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window = null) + { + throw new InvalidOperationException("This method is only intended to be used with SetWindowFields."); + } + /// /// Returns a sequence of values. /// diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs index 2b8ce448dd3..147bd427729 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs @@ -93,6 +93,9 @@ internal enum AstNodeType MatchesEverythingFilter, MatchesNothingFilter, MatchStage, + MedianExpression, + MedianAccumulatorExpression, + MedianWindowExpression, MergeStage, ModFilterOperation, NaryExpression, @@ -104,6 +107,9 @@ internal enum AstNodeType NullaryWindowExpression, OrFilter, OutStage, + PercentileExpression, + PercentileAccumulatorExpression, + PercentileWindowExpression, PickAccumulatorExpression, PickExpression, Pipeline, diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs index 90512554fc6..bf729df32a9 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs @@ -597,6 +597,21 @@ public static AstExpression Max(AstExpression arg1, AstExpression arg2) return new AstNaryExpression(AstNaryOperator.Max, [arg1, arg2]); } + public static AstExpression Median(AstExpression input) + { + return new AstMedianExpression(input); + } + + public static AstAccumulatorExpression MedianAccumulator(AstExpression input) + { + return new AstMedianAccumulatorExpression(input); + } + + public static AstWindowExpression MedianWindowExpression(AstExpression input, AstWindow window) + { + return new AstMedianWindowExpression(input, window); + } + public static AstExpression Min(AstExpression array) { return new AstUnaryExpression(AstUnaryOperator.Min, array); @@ -653,6 +668,21 @@ public static AstExpression Or(params AstExpression[] args) return new AstNaryExpression(AstNaryOperator.Or, flattenedArgs); } + public static AstExpression Percentile(AstExpression input, AstExpression percentiles) + { + return new AstPercentileExpression(input, percentiles); + } + + public static AstAccumulatorExpression PercentileAccumulator(AstExpression input, AstExpression percentiles) + { + return new AstPercentileAccumulatorExpression(input, percentiles); + } + + public static AstWindowExpression PercentileWindowExpression(AstExpression input, AstExpression percentiles, AstWindow window) + { + return new AstPercentileWindowExpression(input, percentiles, window); + } + public static AstExpression PickExpression(AstPickOperator @operator, AstExpression source, AstSortFields sortBy, AstVarExpression @as, AstExpression selector, AstExpression n) { return new AstPickExpression(@operator, source, sortBy, @as, selector, n); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianAccumulatorExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianAccumulatorExpression.cs new file mode 100644 index 00000000000..d7cc2bb085a --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianAccumulatorExpression.cs @@ -0,0 +1,63 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions +{ + internal sealed class AstMedianAccumulatorExpression : AstAccumulatorExpression + { + private readonly AstExpression _input; + + public AstMedianAccumulatorExpression(AstExpression input) + { + _input = Ensure.IsNotNull(input, nameof(input)); + } + + public AstExpression Input => _input; + + public override AstNodeType NodeType => AstNodeType.MedianAccumulatorExpression; + + public override AstNode Accept(AstNodeVisitor visitor) + { + return visitor.VisitMedianAccumulatorExpression(this); + } + + public override BsonValue Render() + { + return new BsonDocument + { + { + "$median", new BsonDocument + { + { "input", _input.Render() }, + { "method", "approximate" } // server requires this parameter but currently only allows this value + } + } + }; + } + + public AstMedianAccumulatorExpression Update(AstExpression input) + { + if (input == _input) + { + return this; + } + return new AstMedianAccumulatorExpression(input); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianExpression.cs new file mode 100644 index 00000000000..a171bb9fa02 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianExpression.cs @@ -0,0 +1,63 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions +{ + internal sealed class AstMedianExpression : AstExpression + { + private readonly AstExpression _input; + + public AstMedianExpression(AstExpression input) + { + _input = Ensure.IsNotNull(input, nameof(input)); + } + + public AstExpression Input => _input; + + public override AstNodeType NodeType => AstNodeType.MedianExpression; + + public override AstNode Accept(AstNodeVisitor visitor) + { + return visitor.VisitMedianExpression(this); + } + + public override BsonValue Render() + { + return new BsonDocument + { + { + "$median", new BsonDocument + { + { "input", _input.Render() }, + { "method", "approximate" } // server requires this parameter but currently only allows this value + } + } + }; + } + + public AstMedianExpression Update(AstExpression input) + { + if (input == _input) + { + return this; + } + return new AstMedianExpression(input); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianWindowExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianWindowExpression.cs new file mode 100644 index 00000000000..fbe71c3f278 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMedianWindowExpression.cs @@ -0,0 +1,69 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions +{ + internal sealed class AstMedianWindowExpression : AstWindowExpression + { + private readonly AstExpression _input; + private readonly AstWindow _window; + + public AstMedianWindowExpression(AstExpression input, AstWindow window) + { + _input = Ensure.IsNotNull(input, nameof(input)); + _window = window; + } + + public AstExpression Input => _input; + + public AstWindow Window => _window; + + public override AstNodeType NodeType => AstNodeType.MedianWindowExpression; + + public override AstNode Accept(AstNodeVisitor visitor) + { + return visitor.VisitMedianWindowExpression(this); + } + + public override BsonValue Render() + { + return new BsonDocument + { + { + "$median", new BsonDocument + { + { "input", _input.Render() }, + { "method", "approximate" } // server requires this parameter but currently only allows this value + } + }, + { "window", _window?.Render(), _window != null } + }; + } + + public AstMedianWindowExpression Update(AstExpression input, AstWindow window) + { + if (input == _input && window == _window) + { + return this; + } + + return new AstMedianWindowExpression(input, window); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileAccumulatorExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileAccumulatorExpression.cs new file mode 100644 index 00000000000..77cee275402 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileAccumulatorExpression.cs @@ -0,0 +1,68 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions +{ + internal sealed class AstPercentileAccumulatorExpression : AstAccumulatorExpression + { + private readonly AstExpression _input; + private readonly AstExpression _percentiles; + + public AstPercentileAccumulatorExpression(AstExpression input, AstExpression percentiles) + { + _input = Ensure.IsNotNull(input, nameof(input)); + _percentiles = Ensure.IsNotNull(percentiles, nameof(percentiles)); + } + + public AstExpression Input => _input; + + public AstExpression Percentiles => _percentiles; + + public override AstNodeType NodeType => AstNodeType.PercentileAccumulatorExpression; + + public override AstNode Accept(AstNodeVisitor visitor) + { + return visitor.VisitPercentileAccumulatorExpression(this); + } + + public override BsonValue Render() + { + return new BsonDocument + { + { + "$percentile", new BsonDocument + { + { "input", _input.Render() }, + { "p", _percentiles.Render() }, + { "method", "approximate" } // server requires this parameter but currently only allows this value + } + } + }; + } + + public AstPercentileAccumulatorExpression Update(AstExpression input, AstExpression percentiles) + { + if (input == _input && percentiles == _percentiles) + { + return this; + } + return new AstPercentileAccumulatorExpression(input, percentiles); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileExpression.cs new file mode 100644 index 00000000000..aab4920143d --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileExpression.cs @@ -0,0 +1,68 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions +{ + internal sealed class AstPercentileExpression : AstExpression + { + private readonly AstExpression _input; + private readonly AstExpression _percentiles; + + public AstPercentileExpression(AstExpression input, AstExpression percentiles) + { + _input = Ensure.IsNotNull(input, nameof(input)); + _percentiles = Ensure.IsNotNull(percentiles, nameof(percentiles)); + } + + public AstExpression Input => _input; + + public AstExpression Percentiles => _percentiles; + + public override AstNodeType NodeType => AstNodeType.PercentileExpression; + + public override AstNode Accept(AstNodeVisitor visitor) + { + return visitor.VisitPercentileExpression(this); + } + + public override BsonValue Render() + { + return new BsonDocument + { + { + "$percentile", new BsonDocument + { + { "input", _input.Render() }, + { "p", _percentiles.Render() }, + { "method", "approximate" } // server requires this parameter but currently only allows this value + } + } + }; + } + + public AstPercentileExpression Update(AstExpression input, AstExpression percentiles) + { + if (input == _input && percentiles == _percentiles) + { + return this; + } + return new AstPercentileExpression(input, percentiles); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileWindowExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileWindowExpression.cs new file mode 100644 index 00000000000..055bcfb8ef3 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstPercentileWindowExpression.cs @@ -0,0 +1,74 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions +{ + internal sealed class AstPercentileWindowExpression : AstWindowExpression + { + private readonly AstExpression _input; + private readonly AstExpression _percentiles; + private readonly AstWindow _window; + + public AstPercentileWindowExpression(AstExpression input, AstExpression percentiles, AstWindow window) + { + _input = Ensure.IsNotNull(input, nameof(input)); + _percentiles = Ensure.IsNotNull(percentiles, nameof(percentiles)); + _window = window; + } + + public AstExpression Input => _input; + + public AstExpression Percentiles => _percentiles; + + public AstWindow Window => _window; + + public override AstNodeType NodeType => AstNodeType.PercentileWindowExpression; + + public override AstNode Accept(AstNodeVisitor visitor) + { + return visitor.VisitPercentileWindowExpression(this); + } + + public override BsonValue Render() + { + return new BsonDocument + { + { + "$percentile", new BsonDocument + { + { "input", _input.Render() }, + { "p", _percentiles.Render() }, + { "method", "approximate" } // server requires this parameter but currently only allows this value + } + }, + { "window", _window?.Render(), _window != null } + }; + } + + public AstPercentileWindowExpression Update(AstExpression input, AstExpression percentiles, AstWindow window) + { + if (input == _input && percentiles == _percentiles && window == _window) + { + return this; + } + + return new AstPercentileWindowExpression(input, percentiles, window); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs index 5967215ee25..37de0673850 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Optimizers/AstGroupingPipelineOptimizer.cs @@ -404,32 +404,67 @@ unaryExpression.Arg is AstGetFieldExpression innerMostGetFieldExpression && public override AstNode VisitMapExpression(AstMapExpression node) { // { $map : { input : { $getField : { input : "$$ROOT", field : "_elements" } }, as : "x", in : f(x) } } => { __agg0 : { $push : f(x => element) } } + "$__agg0" - if (node.Input is AstGetFieldExpression mapInputGetFieldExpression && - mapInputGetFieldExpression.FieldName.IsStringConstant("_elements") && - mapInputGetFieldExpression.Input.IsRootVar()) + if (IsMappedElementsField(node, out var rewrittenArg)) { - var rewrittenArg = (AstExpression)AstNodeReplacer.Replace(node.In, (node.As, _element)); var accumulatorExpression = AstExpression.UnaryAccumulator(AstUnaryAccumulatorOperator.Push, rewrittenArg); - var accumulatorFieldName = _accumulators.AddAccumulatorExpression(accumulatorExpression); - return AstExpression.GetField(AstExpression.RootVar, accumulatorFieldName); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); } return base.VisitMapExpression(node); } + public override AstNode VisitMedianExpression(AstMedianExpression node) + { + // { $median : { input: { $getField : { input : "$$ROOT", field : "_elements" } }, method: "approximate" } } + // => { __agg0 : { $median : { input: element, method: "approximate" } } } + "$__agg0" + if (IsElementsField(node.Input)) + { + var accumulatorExpression = AstExpression.MedianAccumulator(_element); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); + } + + // { $median : { input: { $map : { input : { $getField : { input : "$$ROOT", field : "_elements" } }, as : "x", in : f(x) } }, method: "approximate" } } + // => { __agg0 : { $median : { input: f(x => element), method: "approximate" } } } + "$__agg0" + if (IsMappedElementsField(node.Input, out var rewrittenArg)) + { + var accumulatorExpression = AstExpression.MedianAccumulator(rewrittenArg); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); + } + + return base.VisitMedianExpression(node); + } + + public override AstNode VisitPercentileExpression(AstPercentileExpression node) + { + // { $percentile : { input: { $getField : { input : "$$ROOT", field : "_elements" } }, p: [...], method: "approximate" } } + // => { __agg0 : { $percentile : { input: element, p: [...], method: "approximate" } } } + "$__agg0" + if (IsElementsField(node.Input)) + { + var accumulatorExpression = AstExpression.PercentileAccumulator(_element, node.Percentiles); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); + } + + // { $percentile : { input: { $map : { input : { $getField : { input : "$$ROOT", field : "_elements" } }, as : "x", in : f(x) } }, p: [...], method: "approximate" } } + // => { __agg0 : { $percentile : { input: f(x => element), p: [...], method: "approximate" } } } + "$__agg0" + if (IsMappedElementsField(node.Input, out var rewrittenArg)) + { + var accumulatorExpression = AstExpression.PercentileAccumulator(rewrittenArg, node.Percentiles); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); + } + + return base.VisitPercentileExpression(node); + } + public override AstNode VisitPickExpression(AstPickExpression node) { // { $pickOperator : { source : { $getField : { input : "$$ROOT", field : "_elements" } }, as : "x", sortBy : s, selector : f(x) } } // => { __agg0 : { $pickAccumulatorOperator : { sortBy : s, selector : f(x => element) } } } + "$__agg0" - if (node.Source is AstGetFieldExpression getFieldExpression && - getFieldExpression.Input.IsRootVar() && - getFieldExpression.FieldName.IsStringConstant("_elements")) + if (IsElementsField(node.Source)) { var @operator = node.Operator.ToAccumulatorOperator(); var rewrittenSelector = (AstExpression)AstNodeReplacer.Replace(node.Selector, (node.As, _element)); var accumulatorExpression = new AstPickAccumulatorExpression(@operator, node.SortBy, rewrittenSelector, node.N); - var accumulatorFieldName = _accumulators.AddAccumulatorExpression(accumulatorExpression); - return AstExpression.GetField(AstExpression.RootVar, accumulatorFieldName); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); } return base.VisitPickExpression(node); @@ -437,80 +472,60 @@ public override AstNode VisitPickExpression(AstPickExpression node) public override AstNode VisitUnaryExpression(AstUnaryExpression node) { - if (TryOptimizeSizeOfElements(out var optimizedExpression)) + // { $size : "$_elements" } => { __agg0 : { $sum : 1 } } + "$__agg0" + if (node.Operator == AstUnaryOperator.Size) { - return optimizedExpression; + if (node.Arg is AstGetFieldExpression argGetFieldExpression && + argGetFieldExpression.FieldName.IsStringConstant("_elements")) + { + var accumulatorExpression = AstExpression.UnaryAccumulator(AstUnaryAccumulatorOperator.Sum, 1); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); + } } - if (TryOptimizeAccumulatorOfElements(out optimizedExpression)) + // { $accumulator : { $getField : { input : "$$ROOT", field : "_elements" } } } => { __agg0 : { $accumulator : element } } + "$__agg0" + if (node.Operator.IsAccumulator(out var accumulatorOperator) && IsElementsField(node.Arg)) { - return optimizedExpression; + var accumulatorExpression = AstExpression.UnaryAccumulator(accumulatorOperator, _element); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); } - if (TryOptimizeAccumulatorOfMappedElements(out optimizedExpression)) + // { $accumulator : { $map : { input : { $getField : { input : "$$ROOT", field : "_elements" } }, as : "x", in : f(x) } } } + // => { __agg0 : { $accumulator : f(x => element) } } + "$__agg0" + if (node.Operator.IsAccumulator(out accumulatorOperator) && + IsMappedElementsField(node.Arg, out var rewrittenArg)) { - return optimizedExpression; + var accumulatorExpression = AstExpression.UnaryAccumulator(accumulatorOperator, rewrittenArg); + return CreateGetAccumulatorFieldExpression(accumulatorExpression); } return base.VisitUnaryExpression(node); + } - bool TryOptimizeSizeOfElements(out AstExpression optimizedExpression) - { - // { $size : "$_elements" } => { __agg0 : { $sum : 1 } } + "$__agg0" - if (node.Operator == AstUnaryOperator.Size) - { - if (node.Arg is AstGetFieldExpression argGetFieldExpression && - argGetFieldExpression.FieldName.IsStringConstant("_elements")) - { - var accumulatorExpression = AstExpression.UnaryAccumulator(AstUnaryAccumulatorOperator.Sum, 1); - var accumulatorFieldName = _accumulators.AddAccumulatorExpression(accumulatorExpression); - optimizedExpression = AstExpression.GetField(AstExpression.RootVar, accumulatorFieldName); - return true; - } - } - - optimizedExpression = null; - return false; - } + private bool IsElementsField(AstExpression expression) + { + return + expression is AstGetFieldExpression getFieldExpression && + getFieldExpression.FieldName.IsStringConstant("_elements") && + getFieldExpression.Input.IsRootVar(); + } - bool TryOptimizeAccumulatorOfElements(out AstExpression optimizedExpression) + private bool IsMappedElementsField(AstExpression expression, out AstExpression rewrittenArg) + { + if (expression is AstMapExpression mapExpression && IsElementsField(mapExpression.Input)) { - // { $accumulator : { $getField : { input : "$$ROOT", field : "_elements" } } } => { __agg0 : { $accumulator : element } } + "$__agg0" - if (node.Operator.IsAccumulator(out var accumulatorOperator) && - node.Arg is AstGetFieldExpression getFieldExpression && - getFieldExpression.FieldName.IsStringConstant("_elements") && - getFieldExpression.Input.IsRootVar()) - { - var accumulatorExpression = AstExpression.UnaryAccumulator(accumulatorOperator, _element); - var accumulatorFieldName = _accumulators.AddAccumulatorExpression(accumulatorExpression); - optimizedExpression = AstExpression.GetField(AstExpression.RootVar, accumulatorFieldName); - return true; - } - - optimizedExpression = null; - return false; - + rewrittenArg = (AstExpression)AstNodeReplacer.Replace(mapExpression.In, (mapExpression.As, _element)); + return true; } - bool TryOptimizeAccumulatorOfMappedElements(out AstExpression optimizedExpression) - { - // { $accumulator : { $map : { input : { $getField : { input : "$$ROOT", field : "_elements" } }, as : "x", in : f(x) } } } => { __agg0 : { $accumulator : f(x => element) } } + "$__agg0" - if (node.Operator.IsAccumulator(out var accumulatorOperator) && - node.Arg is AstMapExpression mapExpression && - mapExpression.Input is AstGetFieldExpression mapInputGetFieldExpression && - mapInputGetFieldExpression.FieldName.IsStringConstant("_elements") && - mapInputGetFieldExpression.Input.IsRootVar()) - { - var rewrittenArg = (AstExpression)AstNodeReplacer.Replace(mapExpression.In, (mapExpression.As, _element)); - var accumulatorExpression = AstExpression.UnaryAccumulator(accumulatorOperator, rewrittenArg); - var accumulatorFieldName = _accumulators.AddAccumulatorExpression(accumulatorExpression); - optimizedExpression = AstExpression.GetField(AstExpression.RootVar, accumulatorFieldName); - return true; - } + rewrittenArg = null; + return false; + } - optimizedExpression = null; - return false; - } + private AstExpression CreateGetAccumulatorFieldExpression(AstAccumulatorExpression accumulatorExpression) + { + var fieldName = _accumulators.AddAccumulatorExpression(accumulatorExpression); + return AstExpression.GetField(AstExpression.RootVar, fieldName); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs index 1222d0060e9..83a50bd6f44 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs @@ -504,6 +504,21 @@ public virtual AstNode VisitMatchStage(AstMatchStage node) return node.Update(VisitAndConvert(node.Filter)); } + public virtual AstNode VisitMedianExpression(AstMedianExpression node) + { + return node.Update(VisitAndConvert(node.Input)); + } + + public virtual AstNode VisitMedianAccumulatorExpression(AstMedianAccumulatorExpression node) + { + return node.Update(VisitAndConvert(node.Input)); + } + + public virtual AstNode VisitMedianWindowExpression(AstMedianWindowExpression node) + { + return node.Update(VisitAndConvert(node.Input), node.Window); + } + public virtual AstNode VisitMergeStage(AstMergeStage node) { return node.Update(VisitAndConvert(node.Let)); @@ -559,6 +574,21 @@ public virtual AstNode VisitOutStage(AstOutStage node) return node; } + public virtual AstNode VisitPercentileExpression(AstPercentileExpression node) + { + return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.Percentiles)); + } + + public virtual AstNode VisitPercentileAccumulatorExpression(AstPercentileAccumulatorExpression node) + { + return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.Percentiles)); + } + + public virtual AstNode VisitPercentileWindowExpression(AstPercentileWindowExpression node) + { + return node.Update(VisitAndConvert(node.Input), VisitAndConvert(node.Percentiles), node.Window); + } + public virtual AstNode VisitPickAccumulatorExpression(AstPickAccumulatorExpression node) { return node.Update(node.Operator, node.SortBy, VisitAndConvert(node.Selector), VisitAndConvert(node.N)); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MongoEnumerableMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MongoEnumerableMethod.cs index 3773f1f92bf..c10550024c3 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MongoEnumerableMethod.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MongoEnumerableMethod.cs @@ -25,6 +25,46 @@ internal static class MongoEnumerableMethod private static readonly MethodInfo __allElements; private static readonly MethodInfo __allMatchingElements; private static readonly MethodInfo __firstMatchingElement; + private static readonly MethodInfo __medianDecimal; + private static readonly MethodInfo __medianDecimalWithSelector; + private static readonly MethodInfo __medianDouble; + private static readonly MethodInfo __medianDoubleWithSelector; + private static readonly MethodInfo __medianInt32; + private static readonly MethodInfo __medianInt32WithSelector; + private static readonly MethodInfo __medianInt64; + private static readonly MethodInfo __medianInt64WithSelector; + private static readonly MethodInfo __medianNullableDecimal; + private static readonly MethodInfo __medianNullableDecimalWithSelector; + private static readonly MethodInfo __medianNullableDouble; + private static readonly MethodInfo __medianNullableDoubleWithSelector; + private static readonly MethodInfo __medianNullableInt32; + private static readonly MethodInfo __medianNullableInt32WithSelector; + private static readonly MethodInfo __medianNullableInt64; + private static readonly MethodInfo __medianNullableInt64WithSelector; + private static readonly MethodInfo __medianNullableSingle; + private static readonly MethodInfo __medianNullableSingleWithSelector; + private static readonly MethodInfo __medianSingle; + private static readonly MethodInfo __medianSingleWithSelector; + private static readonly MethodInfo __percentileDecimal; + private static readonly MethodInfo __percentileDecimalWithSelector; + private static readonly MethodInfo __percentileDouble; + private static readonly MethodInfo __percentileDoubleWithSelector; + private static readonly MethodInfo __percentileInt32; + private static readonly MethodInfo __percentileInt32WithSelector; + private static readonly MethodInfo __percentileInt64; + private static readonly MethodInfo __percentileInt64WithSelector; + private static readonly MethodInfo __percentileNullableDecimal; + private static readonly MethodInfo __percentileNullableDecimalWithSelector; + private static readonly MethodInfo __percentileNullableDouble; + private static readonly MethodInfo __percentileNullableDoubleWithSelector; + private static readonly MethodInfo __percentileNullableInt32; + private static readonly MethodInfo __percentileNullableInt32WithSelector; + private static readonly MethodInfo __percentileNullableInt64; + private static readonly MethodInfo __percentileNullableInt64WithSelector; + private static readonly MethodInfo __percentileNullableSingle; + private static readonly MethodInfo __percentileNullableSingleWithSelector; + private static readonly MethodInfo __percentileSingle; + private static readonly MethodInfo __percentileSingleWithSelector; private static readonly MethodInfo __whereWithLimit; // static constructor @@ -33,6 +73,46 @@ static MongoEnumerableMethod() __allElements = ReflectionInfo.Method((IEnumerable source) => source.AllElements()); __allMatchingElements = ReflectionInfo.Method((IEnumerable source, string identifier) => source.AllMatchingElements(identifier)); __firstMatchingElement = ReflectionInfo.Method((IEnumerable source) => source.FirstMatchingElement()); + __medianDecimal = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianDecimalWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianDouble = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianDoubleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianInt32 = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianInt32WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianInt64 = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianInt64WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianNullableDecimal = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianNullableDecimalWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianNullableDouble = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianNullableDoubleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianNullableInt32 = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianNullableInt32WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianNullableInt64 = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianNullableInt64WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianNullableSingle = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianNullableSingleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __medianSingle = ReflectionInfo.Method((IEnumerable source) => source.Median()); + __medianSingleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector) => source.Median(selector)); + __percentileDecimal = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileDecimalWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileDouble = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileDoubleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileInt32 = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileInt32WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileInt64 = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileInt64WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileNullableDecimal = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileNullableDecimalWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileNullableDouble = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileNullableDoubleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileNullableInt32 = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileNullableInt32WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileNullableInt64 = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileNullableInt64WithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileNullableSingle = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileNullableSingleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); + __percentileSingle = ReflectionInfo.Method((IEnumerable source, IEnumerable percentiles) => source.Percentile(percentiles)); + __percentileSingleWithSelector = ReflectionInfo.Method((IEnumerable source, Func selector, IEnumerable percentiles) => source.Percentile(selector, percentiles)); __whereWithLimit = ReflectionInfo.Method((IEnumerable source, Func predicate, int limit) => source.Where(predicate, limit)); } @@ -40,6 +120,46 @@ static MongoEnumerableMethod() public static MethodInfo AllElements => __allElements; public static MethodInfo AllMatchingElements => __allMatchingElements; public static MethodInfo FirstMatchingElement => __firstMatchingElement; + public static MethodInfo MedianDecimal => __medianDecimal; + public static MethodInfo MedianDecimalWithSelector => __medianDecimalWithSelector; + public static MethodInfo MedianDouble => __medianDouble; + public static MethodInfo MedianDoubleWithSelector => __medianDoubleWithSelector; + public static MethodInfo MedianInt32 => __medianInt32; + public static MethodInfo MedianInt32WithSelector => __medianInt32WithSelector; + public static MethodInfo MedianInt64 => __medianInt64; + public static MethodInfo MedianInt64WithSelector => __medianInt64WithSelector; + public static MethodInfo MedianNullableDecimal => __medianNullableDecimal; + public static MethodInfo MedianNullableDecimalWithSelector => __medianNullableDecimalWithSelector; + public static MethodInfo MedianNullableDouble => __medianNullableDouble; + public static MethodInfo MedianNullableDoubleWithSelector => __medianNullableDoubleWithSelector; + public static MethodInfo MedianNullableInt32 => __medianNullableInt32; + public static MethodInfo MedianNullableInt32WithSelector => __medianNullableInt32WithSelector; + public static MethodInfo MedianNullableInt64 => __medianNullableInt64; + public static MethodInfo MedianNullableInt64WithSelector => __medianNullableInt64WithSelector; + public static MethodInfo MedianNullableSingle => __medianNullableSingle; + public static MethodInfo MedianNullableSingleWithSelector => __medianNullableSingleWithSelector; + public static MethodInfo MedianSingle => __medianSingle; + public static MethodInfo MedianSingleWithSelector => __medianSingleWithSelector; + public static MethodInfo PercentileDecimal => __percentileDecimal; + public static MethodInfo PercentileDecimalWithSelector => __percentileDecimalWithSelector; + public static MethodInfo PercentileDouble => __percentileDouble; + public static MethodInfo PercentileDoubleWithSelector => __percentileDoubleWithSelector; + public static MethodInfo PercentileInt32 => __percentileInt32; + public static MethodInfo PercentileInt32WithSelector => __percentileInt32WithSelector; + public static MethodInfo PercentileInt64 => __percentileInt64; + public static MethodInfo PercentileInt64WithSelector => __percentileInt64WithSelector; + public static MethodInfo PercentileNullableDecimal => __percentileNullableDecimal; + public static MethodInfo PercentileNullableDecimalWithSelector => __percentileNullableDecimalWithSelector; + public static MethodInfo PercentileNullableDouble => __percentileNullableDouble; + public static MethodInfo PercentileNullableDoubleWithSelector => __percentileNullableDoubleWithSelector; + public static MethodInfo PercentileNullableInt32 => __percentileNullableInt32; + public static MethodInfo PercentileNullableInt32WithSelector => __percentileNullableInt32WithSelector; + public static MethodInfo PercentileNullableInt64 => __percentileNullableInt64; + public static MethodInfo PercentileNullableInt64WithSelector => __percentileNullableInt64WithSelector; + public static MethodInfo PercentileNullableSingle => __percentileNullableSingle; + public static MethodInfo PercentileNullableSingleWithSelector => __percentileNullableSingleWithSelector; + public static MethodInfo PercentileSingle => __percentileSingle; + public static MethodInfo PercentileSingleWithSelector => __percentileSingleWithSelector; public static MethodInfo WhereWithLimit => __whereWithLimit; } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/WindowMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/WindowMethod.cs index 374caf1f787..693e8762269 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/WindowMethod.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/WindowMethod.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using System.Reflection; namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection @@ -84,7 +85,27 @@ internal static class WindowMethod private static readonly MethodInfo __last; private static readonly MethodInfo __locf; private static readonly MethodInfo __max; + private static readonly MethodInfo __medianWithDecimal; + private static readonly MethodInfo __medianWithDouble; + private static readonly MethodInfo __medianWithInt32; + private static readonly MethodInfo __medianWithInt64; + private static readonly MethodInfo __medianWithNullableDecimal; + private static readonly MethodInfo __medianWithNullableDouble; + private static readonly MethodInfo __medianWithNullableInt32; + private static readonly MethodInfo __medianWithNullableInt64; + private static readonly MethodInfo __medianWithNullableSingle; + private static readonly MethodInfo __medianWithSingle; private static readonly MethodInfo __min; + private static readonly MethodInfo __percentileWithDecimal; + private static readonly MethodInfo __percentileWithDouble; + private static readonly MethodInfo __percentileWithInt32; + private static readonly MethodInfo __percentileWithInt64; + private static readonly MethodInfo __percentileWithNullableDecimal; + private static readonly MethodInfo __percentileWithNullableDouble; + private static readonly MethodInfo __percentileWithNullableInt32; + private static readonly MethodInfo __percentileWithNullableInt64; + private static readonly MethodInfo __percentileWithNullableSingle; + private static readonly MethodInfo __percentileWithSingle; private static readonly MethodInfo __push; private static readonly MethodInfo __rank; private static readonly MethodInfo __shift; @@ -186,7 +207,27 @@ static WindowMethod() __last = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Last(selector, window)); __locf = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Locf(selector, window)); __max = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Max(selector, window)); + __medianWithDecimal = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithDouble = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithInt32 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithInt64 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithNullableDecimal = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithNullableDouble = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithNullableInt32 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithNullableInt64 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithNullableSingle = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); + __medianWithSingle = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Median(selector, window)); __min = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Min(selector, window)); + __percentileWithDecimal = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithDouble = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithInt32 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithInt64 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithNullableDecimal = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithNullableDouble = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithNullableInt32 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithNullableInt64 = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithNullableSingle = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); + __percentileWithSingle = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, IEnumerable percentiles, SetWindowFieldsWindow window) => partition.Percentile(selector, percentiles, window)); __push = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, SetWindowFieldsWindow window) => partition.Push(selector, window)); __rank = ReflectionInfo.Method((ISetWindowFieldsPartition partition) => partition.Rank()); __shift = ReflectionInfo.Method((ISetWindowFieldsPartition partition, Func selector, int by) => partition.Shift(selector, by)); @@ -287,7 +328,27 @@ static WindowMethod() public static MethodInfo Last => __last; public static MethodInfo Locf => __locf; public static MethodInfo Max => __max; + public static MethodInfo MedianWithDecimal => __medianWithDecimal; + public static MethodInfo MedianWithDouble => __medianWithDouble; + public static MethodInfo MedianWithInt32 => __medianWithInt32; + public static MethodInfo MedianWithInt64 => __medianWithInt64; + public static MethodInfo MedianWithNullableDecimal => __medianWithNullableDecimal; + public static MethodInfo MedianWithNullableDouble => __medianWithNullableDouble; + public static MethodInfo MedianWithNullableInt32 => __medianWithNullableInt32; + public static MethodInfo MedianWithNullableInt64 => __medianWithNullableInt64; + public static MethodInfo MedianWithNullableSingle => __medianWithNullableSingle; + public static MethodInfo MedianWithSingle => __medianWithSingle; public static MethodInfo Min => __min; + public static MethodInfo PercentileWithDecimal => __percentileWithDecimal; + public static MethodInfo PercentileWithDouble => __percentileWithDouble; + public static MethodInfo PercentileWithInt32 => __percentileWithInt32; + public static MethodInfo PercentileWithInt64 => __percentileWithInt64; + public static MethodInfo PercentileWithNullableDecimal => __percentileWithNullableDecimal; + public static MethodInfo PercentileWithNullableDouble => __percentileWithNullableDouble; + public static MethodInfo PercentileWithNullableInt32 => __percentileWithNullableInt32; + public static MethodInfo PercentileWithNullableInt64 => __percentileWithNullableInt64; + public static MethodInfo PercentileWithNullableSingle => __percentileWithNullableSingle; + public static MethodInfo PercentileWithSingle => __percentileWithSingle; public static MethodInfo Push => __push; public static MethodInfo Rank => __rank; public static MethodInfo Shift => __shift; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs index ae0c1f93193..4dbbe32fdd7 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs @@ -64,8 +64,10 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC case "IsNullOrWhiteSpace": return IsNullOrWhiteSpaceMethodToAggregationExpressionTranslator.Translate(context, expression); case "IsSubsetOf": return IsSubsetOfMethodToAggregationExpressionTranslator.Translate(context, expression); case "Locf": return LocfMethodToAggregationExpressionTranslator.Translate(context, expression); + case "Median": return MedianMethodToAggregationExpressionTranslator.Translate(context, expression); case "OfType": return OfTypeMethodToAggregationExpressionTranslator.Translate(context, expression); case "Parse": return ParseMethodToAggregationExpressionTranslator.Translate(context, expression); + case "Percentile": return PercentileMethodToAggregationExpressionTranslator.Translate(context, expression); case "Pow": return PowMethodToAggregationExpressionTranslator.Translate(context, expression); case "Push": return PushMethodToAggregationExpressionTranslator.Translate(context, expression); case "Range": return RangeMethodToAggregationExpressionTranslator.Translate(context, expression); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/MedianMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/MedianMethodToAggregationExpressionTranslator.cs new file mode 100644 index 00000000000..0baa8709c1d --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/MedianMethodToAggregationExpressionTranslator.cs @@ -0,0 +1,107 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq.Expressions; +using System.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + internal class MedianMethodToAggregationExpressionTranslator + { + private static readonly MethodInfo[] __medianMethods = + [ + MongoEnumerableMethod.MedianDecimal, + MongoEnumerableMethod.MedianDecimalWithSelector, + MongoEnumerableMethod.MedianDouble, + MongoEnumerableMethod.MedianDoubleWithSelector, + MongoEnumerableMethod.MedianInt32, + MongoEnumerableMethod.MedianInt32WithSelector, + MongoEnumerableMethod.MedianInt64, + MongoEnumerableMethod.MedianInt64WithSelector, + MongoEnumerableMethod.MedianNullableDecimal, + MongoEnumerableMethod.MedianNullableDecimalWithSelector, + MongoEnumerableMethod.MedianNullableDouble, + MongoEnumerableMethod.MedianNullableDoubleWithSelector, + MongoEnumerableMethod.MedianNullableInt32, + MongoEnumerableMethod.MedianNullableInt32WithSelector, + MongoEnumerableMethod.MedianNullableInt64, + MongoEnumerableMethod.MedianNullableInt64WithSelector, + MongoEnumerableMethod.MedianNullableSingle, + MongoEnumerableMethod.MedianNullableSingleWithSelector, + MongoEnumerableMethod.MedianSingle, + MongoEnumerableMethod.MedianSingleWithSelector + ]; + + private static readonly MethodInfo[] __medianWithSelectorMethods = + [ + MongoEnumerableMethod.MedianDecimalWithSelector, + MongoEnumerableMethod.MedianDoubleWithSelector, + MongoEnumerableMethod.MedianInt32WithSelector, + MongoEnumerableMethod.MedianInt64WithSelector, + MongoEnumerableMethod.MedianNullableDecimalWithSelector, + MongoEnumerableMethod.MedianNullableDoubleWithSelector, + MongoEnumerableMethod.MedianNullableInt32WithSelector, + MongoEnumerableMethod.MedianNullableInt64WithSelector, + MongoEnumerableMethod.MedianNullableSingleWithSelector, + MongoEnumerableMethod.MedianSingleWithSelector + ]; + + public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + if (method.IsOneOf(__medianMethods)) + { + var sourceExpression = arguments[0]; + var sourceTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(context, sourceExpression); + NestedAsQueryableHelper.EnsureQueryableMethodHasNestedAsQueryableSource(expression, sourceTranslation); + + var inputAst = sourceTranslation.Ast; + + if (method.IsOneOf(__medianWithSelectorMethods)) + { + var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer); + + var selectorLambda = (LambdaExpression)arguments[1]; + var selectorParameter = selectorLambda.Parameters[0]; + var selectorParameterSymbol = context.CreateSymbol(selectorParameter, sourceItemSerializer); + var selectorContext = context.WithSymbol(selectorParameterSymbol); + var selectorTranslation = ExpressionToAggregationExpressionTranslator.Translate(selectorContext, selectorLambda.Body); + + inputAst = AstExpression.Map( + input: sourceTranslation.Ast, + @as: selectorParameterSymbol.Var, + @in: selectorTranslation.Ast); + } + + var ast = AstExpression.Median(inputAst); + var serializer = StandardSerializers.GetSerializer(expression.Type); + return new TranslatedExpression(expression, ast, serializer); + } + + if (WindowMethodToAggregationExpressionTranslator.CanTranslate(expression)) + { + return WindowMethodToAggregationExpressionTranslator.Translate(context, expression); + } + + throw new ExpressionNotSupportedException(expression); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/PercentileMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/PercentileMethodToAggregationExpressionTranslator.cs new file mode 100644 index 00000000000..216d89f1c49 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/PercentileMethodToAggregationExpressionTranslator.cs @@ -0,0 +1,110 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Linq.Expressions; +using System.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + internal class PercentileMethodToAggregationExpressionTranslator + { + private static readonly MethodInfo[] __percentileMethods = + [ + MongoEnumerableMethod.PercentileDecimal, + MongoEnumerableMethod.PercentileDecimalWithSelector, + MongoEnumerableMethod.PercentileDouble, + MongoEnumerableMethod.PercentileDoubleWithSelector, + MongoEnumerableMethod.PercentileInt32, + MongoEnumerableMethod.PercentileInt32WithSelector, + MongoEnumerableMethod.PercentileInt64, + MongoEnumerableMethod.PercentileInt64WithSelector, + MongoEnumerableMethod.PercentileNullableDecimal, + MongoEnumerableMethod.PercentileNullableDecimalWithSelector, + MongoEnumerableMethod.PercentileNullableDouble, + MongoEnumerableMethod.PercentileNullableDoubleWithSelector, + MongoEnumerableMethod.PercentileNullableInt32, + MongoEnumerableMethod.PercentileNullableInt32WithSelector, + MongoEnumerableMethod.PercentileNullableInt64, + MongoEnumerableMethod.PercentileNullableInt64WithSelector, + MongoEnumerableMethod.PercentileNullableSingle, + MongoEnumerableMethod.PercentileNullableSingleWithSelector, + MongoEnumerableMethod.PercentileSingle, + MongoEnumerableMethod.PercentileSingleWithSelector + ]; + + private static readonly MethodInfo[] __percentileWithSelectorMethods = + [ + MongoEnumerableMethod.PercentileDecimalWithSelector, + MongoEnumerableMethod.PercentileDoubleWithSelector, + MongoEnumerableMethod.PercentileInt32WithSelector, + MongoEnumerableMethod.PercentileInt64WithSelector, + MongoEnumerableMethod.PercentileNullableDecimalWithSelector, + MongoEnumerableMethod.PercentileNullableDoubleWithSelector, + MongoEnumerableMethod.PercentileNullableInt32WithSelector, + MongoEnumerableMethod.PercentileNullableInt64WithSelector, + MongoEnumerableMethod.PercentileNullableSingleWithSelector, + MongoEnumerableMethod.PercentileSingleWithSelector + ]; + + public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + if (method.IsOneOf(__percentileMethods)) + { + var sourceExpression = arguments[0]; + var sourceTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(context, sourceExpression); + NestedAsQueryableHelper.EnsureQueryableMethodHasNestedAsQueryableSource(expression, sourceTranslation); + + var inputAst = sourceTranslation.Ast; + + if (method.IsOneOf(__percentileWithSelectorMethods)) + { + var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer); + + var selectorLambda = (LambdaExpression)arguments[1]; + var selectorParameter = selectorLambda.Parameters[0]; + var selectorParameterSymbol = context.CreateSymbol(selectorParameter, sourceItemSerializer); + var selectorContext = context.WithSymbol(selectorParameterSymbol); + var selectorTranslation = ExpressionToAggregationExpressionTranslator.Translate(selectorContext, selectorLambda.Body); + + inputAst = AstExpression.Map( + input: sourceTranslation.Ast, + @as: selectorParameterSymbol.Var, + @in: selectorTranslation.Ast); + } + + var percentilesExpression = arguments[arguments.Count - 1]; + var percentilesTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(context, percentilesExpression); + + var ast = AstExpression.Percentile(inputAst, percentilesTranslation.Ast); + var serializer = StandardSerializers.GetSerializer(expression.Type); + return new TranslatedExpression(expression, ast, serializer); + } + + if (WindowMethodToAggregationExpressionTranslator.CanTranslate(expression)) + { + return WindowMethodToAggregationExpressionTranslator.Translate(context, expression); + } + + throw new ExpressionNotSupportedException(expression); + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslator.cs index 9b54a5fdd18..f45cffc3e49 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslator.cs @@ -94,7 +94,27 @@ internal static class WindowMethodToAggregationExpressionTranslator WindowMethod.Last, WindowMethod.Locf, WindowMethod.Max, + WindowMethod.MedianWithDecimal, + WindowMethod.MedianWithDouble, + WindowMethod.MedianWithInt32, + WindowMethod.MedianWithInt64, + WindowMethod.MedianWithNullableDecimal, + WindowMethod.MedianWithNullableDouble, + WindowMethod.MedianWithNullableInt32, + WindowMethod.MedianWithNullableInt64, + WindowMethod.MedianWithNullableSingle, + WindowMethod.MedianWithSingle, WindowMethod.Min, + WindowMethod.PercentileWithDecimal, + WindowMethod.PercentileWithDouble, + WindowMethod.PercentileWithInt32, + WindowMethod.PercentileWithInt64, + WindowMethod.PercentileWithNullableDecimal, + WindowMethod.PercentileWithNullableDouble, + WindowMethod.PercentileWithNullableInt32, + WindowMethod.PercentileWithNullableInt64, + WindowMethod.PercentileWithNullableSingle, + WindowMethod.PercentileWithSingle, WindowMethod.Push, WindowMethod.Rank, WindowMethod.Shift, @@ -253,6 +273,30 @@ internal static class WindowMethodToAggregationExpressionTranslator WindowMethod.ShiftWithDefaultValue }; + private static readonly MethodInfo[] __quantileMethods = + [ + WindowMethod.MedianWithDecimal, + WindowMethod.MedianWithDouble, + WindowMethod.MedianWithInt32, + WindowMethod.MedianWithInt64, + WindowMethod.MedianWithNullableDecimal, + WindowMethod.MedianWithNullableDouble, + WindowMethod.MedianWithNullableInt32, + WindowMethod.MedianWithNullableInt64, + WindowMethod.MedianWithNullableSingle, + WindowMethod.MedianWithSingle, + WindowMethod.PercentileWithDecimal, + WindowMethod.PercentileWithDouble, + WindowMethod.PercentileWithInt32, + WindowMethod.PercentileWithInt64, + WindowMethod.PercentileWithNullableDecimal, + WindowMethod.PercentileWithNullableDouble, + WindowMethod.PercentileWithNullableInt32, + WindowMethod.PercentileWithNullableInt64, + WindowMethod.PercentileWithNullableSingle, + WindowMethod.PercentileWithSingle + ]; + public static bool CanTranslate(MethodCallExpression expression) { return expression.Method.IsOneOf(__windowMethods); @@ -339,6 +383,27 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC return new TranslatedExpression(expression, ast, serializer); } + if (method.IsOneOf(__quantileMethods)) + { + ThrowIfSelectorTranslationIsNull(selectorTranslation); + AstExpression ast; + + if (method.Name == "Percentile") + { + // Get the percentiles parameter + var percentilesExpression = GetArgument(parameters, "percentiles", arguments); + var percentilesTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(context, percentilesExpression); + ast = AstExpression.PercentileWindowExpression(selectorTranslation.Ast, percentilesTranslation.Ast, window); + } + else + { + ast = AstExpression.MedianWindowExpression(selectorTranslation.Ast, window); + } + + var serializer = StandardSerializers.GetSerializer(method.ReturnType); + return new TranslatedExpression(expression, ast, serializer); + } + if (method.IsOneOf(__shiftMethods)) { ThrowIfSelectorTranslationIsNull(selectorTranslation); diff --git a/src/MongoDB.Driver/Linq/MongoEnumerable.cs b/src/MongoDB.Driver/Linq/MongoEnumerable.cs index 787da860a4e..eda95082fe0 100644 --- a/src/MongoDB.Driver/Linq/MongoEnumerable.cs +++ b/src/MongoDB.Driver/Linq/MongoEnumerable.cs @@ -16,8 +16,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Linq.Linq3Implementation.Misc; @@ -238,6 +236,226 @@ public static IEnumerable MaxN( throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); } + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static decimal Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static decimal? Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static double Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static double? Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static float Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static float? Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static double Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static double? Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static double Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The sequence of values. + /// The median value. + public static double? Median(this IEnumerable source) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static decimal Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static decimal? Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static double Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static double? Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static float Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static float? Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static double Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static double? Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static double Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes the median of a sequence of values. + /// + /// The type of the elements in the source sequence. + /// A sequence of values to calculate the median of. + /// A transform function to apply to each element. + /// The median value. + public static double? Median(this IEnumerable source, Func selector) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + /// /// Returns the min n results. /// @@ -275,6 +493,246 @@ public static IEnumerable MinN( throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); } + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static decimal[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static decimal?[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double?[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static float[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static float?[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double?[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// A sequence of values to calculate the percentiles of. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double?[] Percentile(this IEnumerable source, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static decimal[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static decimal?[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double?[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static float[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static float?[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double?[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + + /// + /// Computes multiple percentiles of a sequence of values. + /// + /// The type of the elements of . + /// A sequence of values to calculate the percentiles of. + /// A transform function to apply to each element. + /// The percentiles to compute (each between 0.0 and 1.0). + /// The percentiles of the sequence of values. + public static double?[] Percentile(this IEnumerable source, Func selector, IEnumerable percentiles) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + /// /// Computes the population standard deviation of a sequence of values. /// diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/MedianMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/MedianMethodToAggregationExpressionTranslatorTests.cs new file mode 100644 index 00000000000..2013568bb50 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/MedianMethodToAggregationExpressionTranslatorTests.cs @@ -0,0 +1,460 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq; +using MongoDB.Driver.TestHelpers; +using MongoDB.TestHelpers.XunitExtensions; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + public class MedianMethodToAggregationExpressionTranslatorTests : LinqIntegrationTest + { + public MedianMethodToAggregationExpressionTranslatorTests(ClassFixture fixture) + : base(fixture, server => server.Supports(Feature.MedianOperator)) + { + } + + [Theory] + [ParameterAttributeData] + public void Median_with_decimals_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Decimals.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.Decimals.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$Decimals', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(1.0M, 1.0M, 2.0M); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_decimals_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Decimals.AsQueryable().Median(y => y * 2.0M)) : + collection.AsQueryable().Select(x => x.Decimals.Median(y => y * 2.0M)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$Decimals', as : 'y', in : { $multiply : ['$$y', NumberDecimal(2)] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2.0M, 2.0M, 4.0M); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_doubles_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Doubles.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.Doubles.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$Doubles', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(1.0, 1.0, 2.0); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_doubles_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Doubles.AsQueryable().Median(y => y * 2.0)) : + collection.AsQueryable().Select(x => x.Doubles.Median(y => y * 2.0)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$Doubles', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2.0, 2.0, 4.0); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_floats_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Floats.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.Floats.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$Floats', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(1.0F, 1.0F, 2.0F); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_floats_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Floats.AsQueryable().Median(y => y * 2.0F)) : + collection.AsQueryable().Select(x => x.Floats.Median(y => y * 2.0F)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$Floats', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2.0F, 2.0F, 4.0F); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_ints_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Ints.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.Ints.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$Ints', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(1, 1, 2); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_ints_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Ints.AsQueryable().Median(y => y * 2)) : + collection.AsQueryable().Select(x => x.Ints.Median(y => y * 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$Ints', as : 'y', in : { $multiply : ['$$y', 2] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 4); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_longs_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Longs.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.Longs.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$Longs', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(1, 1, 2); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_longs_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Longs.AsQueryable().Median(y => y * 2L)) : + collection.AsQueryable().Select(x => x.Longs.Median(y => y * 2L)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$Longs', as : 'y', in : { $multiply : ['$$y', NumberLong(2)] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 4); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_decimals_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDecimals.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.NullableDecimals.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$NullableDecimals', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 2.0M); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_decimals_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDecimals.AsQueryable().Median(y => y * 2.0M)) : + collection.AsQueryable().Select(x => x.NullableDecimals.Median(y => y * 2.0M)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$NullableDecimals', as : 'y', in : { $multiply : ['$$y', NumberDecimal(2)] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 4.0M); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_doubles_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDoubles.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.NullableDoubles.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$NullableDoubles', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 2.0); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_doubles_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDoubles.AsQueryable().Median(y => y * 2.0)) : + collection.AsQueryable().Select(x => x.NullableDoubles.Median(y => y * 2.0)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$NullableDoubles', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 4.0); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_floats_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableFloats.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.NullableFloats.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$NullableFloats', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 2.0F); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_floats_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableFloats.AsQueryable().Median(y => y * 2.0F)) : + collection.AsQueryable().Select(x => x.NullableFloats.Median(y => y * 2.0F)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$NullableFloats', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 4.0F); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_ints_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableInts.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.NullableInts.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$NullableInts', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 2); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_ints_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableInts.AsQueryable().Median(y => y * 2)) : + collection.AsQueryable().Select(x => x.NullableInts.Median(y => y * 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$NullableInts', as : 'y', in : { $multiply : ['$$y', 2] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 4); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_longs_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableLongs.AsQueryable().Median()) : + collection.AsQueryable().Select(x => x.NullableLongs.Median()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : '$NullableLongs', method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 2); + } + + [Theory] + [ParameterAttributeData] + public void Median_with_nullable_longs_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableLongs.AsQueryable().Median(y => y * 2L)) : + collection.AsQueryable().Select(x => x.NullableLongs.Median(y => y * 2L)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $median : { input : { $map : { input : '$NullableLongs', as : 'y', in : { $multiply : ['$$y', NumberLong(2)] } } }, method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(null, null, 4); + } + + public class C + { + public int Id { get; set; } + [BsonRepresentation(BsonType.Decimal128)] public decimal[] Decimals { get; set; } + public double[] Doubles { get; set; } + public float[] Floats { get; set; } + public int[] Ints { get; set; } + public long[] Longs { get; set; } + [BsonRepresentation(BsonType.Decimal128)] public decimal?[] NullableDecimals { get; set; } + public double?[] NullableDoubles { get; set; } + public float?[] NullableFloats { get; set; } + public int?[] NullableInts { get; set; } + public long?[] NullableLongs { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new() + { + Id = 1, + Decimals = [1.0M], + Doubles = [1.0], + Floats = [1.0F], + Ints = [1], + Longs = [1L], + NullableDecimals = [], + NullableDoubles = [], + NullableFloats = [], + NullableInts = [], + NullableLongs = [] + }, + new() + { + Id = 2, + Decimals = [1.0M, 2.0M], + Doubles = [1.0, 2.0], + Floats = [1.0F, 2.0F], + Ints = [1, 2], + Longs = [1L, 2L], + NullableDecimals = [null], + NullableDoubles = [null], + NullableFloats = [null], + NullableInts = [null], + NullableLongs = [null] + }, + new() + { + Id = 3, + Decimals = [1.0M, 2.0M, 3.0M], + Doubles = [1.0, 2.0, 3.0], + Floats = [1.0F, 2.0F, 3.0F], + Ints = [1, 2, 3], + Longs = [1L, 2L, 3L], + NullableDecimals = [null, 1.0M, 2.0M, 3.0M], + NullableDoubles = [null, 1.0, 2.0, 3.0], + NullableFloats = [null, 1.0F, 2.0F, 3.0F], + NullableInts = [null, 1, 2, 3], + NullableLongs = [null, 1L, 2L, 3L] + } + ]; + } + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/PercentileMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/PercentileMethodToAggregationExpressionTranslatorTests.cs new file mode 100644 index 00000000000..c0f1225c245 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/PercentileMethodToAggregationExpressionTranslatorTests.cs @@ -0,0 +1,541 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq; +using MongoDB.Driver.TestHelpers; +using MongoDB.TestHelpers.XunitExtensions; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + public class PercentileMethodToAggregationExpressionTranslatorTests : LinqIntegrationTest + { + public PercentileMethodToAggregationExpressionTranslatorTests(ClassFixture fixture) + : base(fixture, server => server.Supports(Feature.PercentileOperator)) + { + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_decimals_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Decimals.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Decimals.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Decimals', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0M); + results[1].Should().Equal(1.0M); + results[2].Should().Equal(2.0M); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_decimals_multiple_percentiles_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Decimals.AsQueryable().Percentile(new[] { 0.25, 0.75 })) : + collection.AsQueryable().Select(x => x.Decimals.Percentile(new[] { 0.25, 0.75 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Decimals', p : [0.25, 0.75], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0M, 1.0M); + results[1].Should().Equal(1.0M, 2.0M); + results[2].Should().Equal(1.0M, 3.0M); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_decimals_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Decimals.AsQueryable().Percentile(y => y * 2.0M, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Decimals.Percentile(y => y * 2.0M, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$Decimals', as : 'y', in : { $multiply : ['$$y', NumberDecimal(2)] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(2.0M); + results[1].Should().Equal(2.0M); + results[2].Should().Equal(4.0M); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_doubles_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Doubles.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Doubles.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Doubles', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0); + results[1].Should().Equal(1.0); + results[2].Should().Equal(2.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_doubles_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Doubles.AsQueryable().Percentile(y => y * 2.0, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Doubles.Percentile(y => y * 2.0, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$Doubles', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(2.0); + results[1].Should().Equal(2.0); + results[2].Should().Equal(4.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_floats_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Floats.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Floats.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Floats', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0F); + results[1].Should().Equal(1.0F); + results[2].Should().Equal(2.0F); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_floats_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Floats.AsQueryable().Percentile(y => y * 2.0F, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Floats.Percentile(y => y * 2.0F, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$Floats', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(2.0F); + results[1].Should().Equal(2.0F); + results[2].Should().Equal(4.0F); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_ints_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Ints.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Ints.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Ints', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0); + results[1].Should().Equal(1.0); + results[2].Should().Equal(2.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_ints_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Ints.AsQueryable().Percentile(y => y * 2, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Ints.Percentile(y => y * 2, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$Ints', as : 'y', in : { $multiply : ['$$y', 2] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(2.0); + results[1].Should().Equal(2.0); + results[2].Should().Equal(4.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_longs_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Longs.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Longs.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Longs', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0); + results[1].Should().Equal(1.0); + results[2].Should().Equal(2.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_longs_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.Longs.AsQueryable().Percentile(y => y * 2L, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.Longs.Percentile(y => y * 2L, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$Longs', as : 'y', in : { $multiply : ['$$y', NumberLong(2)] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(2.0); + results[1].Should().Equal(2.0); + results[2].Should().Equal(4.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_decimals_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDecimals.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableDecimals.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$NullableDecimals', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((decimal?)null); + results[1].Should().Equal((decimal?)null); + results[2].Should().Equal(2.0M); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_decimals_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDecimals.AsQueryable().Percentile(y => y * 2.0M, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableDecimals.Percentile(y => y * 2.0M, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$NullableDecimals', as : 'y', in : { $multiply : ['$$y', NumberDecimal(2)] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((decimal?)null); + results[1].Should().Equal((decimal?)null); + results[2].Should().Equal(4.0M); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_doubles_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDoubles.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableDoubles.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$NullableDoubles', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((double?)null); + results[1].Should().Equal((double?)null); + results[2].Should().Equal(2.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_doubles_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableDoubles.AsQueryable().Percentile(y => y * 2.0, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableDoubles.Percentile(y => y * 2.0, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$NullableDoubles', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((double?)null); + results[1].Should().Equal((double?)null); + results[2].Should().Equal(4.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_floats_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableFloats.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableFloats.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$NullableFloats', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((float?)null); + results[1].Should().Equal((float?)null); + results[2].Should().Equal(2.0F); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_floats_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableFloats.AsQueryable().Percentile(y => y * 2.0F, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableFloats.Percentile(y => y * 2.0F, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$NullableFloats', as : 'y', in : { $multiply : ['$$y', 2.0] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((float?)null); + results[1].Should().Equal((float?)null); + results[2].Should().Equal(4.0F); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_ints_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableInts.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableInts.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$NullableInts', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((double?)null); + results[1].Should().Equal((double?)null); + results[2].Should().Equal(2.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_ints_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableInts.AsQueryable().Percentile(y => y * 2, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableInts.Percentile(y => y * 2, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$NullableInts', as : 'y', in : { $multiply : ['$$y', 2] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((double?)null); + results[1].Should().Equal((double?)null); + results[2].Should().Equal(4.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_longs_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableLongs.AsQueryable().Percentile(new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableLongs.Percentile(new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$NullableLongs', p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((double?)null); + results[1].Should().Equal((double?)null); + results[2].Should().Equal(2.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_nullable_longs_selector_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(x => x.NullableLongs.AsQueryable().Percentile(y => y * 2L, new[] { 0.5 })) : + collection.AsQueryable().Select(x => x.NullableLongs.Percentile(y => y * 2L, new[] { 0.5 })); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : { $map : { input : '$NullableLongs', as : 'y', in : { $multiply : ['$$y', NumberLong(2)] } } }, p : [0.5], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal((double?)null); + results[1].Should().Equal((double?)null); + results[2].Should().Equal(4.0); + } + + [Theory] + [ParameterAttributeData] + public void Percentile_with_list_input_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var percentiles = new List { 0.25, 0.5, 0.75 }; + + var queryable = withNestedAsQueryable + ? collection.AsQueryable().Select(x => x.Doubles.AsQueryable().Percentile(percentiles)) + : collection.AsQueryable().Select(x => x.Doubles.Percentile(percentiles)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $percentile : { input : '$Doubles', p : [0.25, 0.5, 0.75], method : 'approximate' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results[0].Should().Equal(1.0, 1.0, 1.0); + results[1].Should().Equal(1.0, 1.0, 2.0); + results[2].Should().Equal(1.0, 2.0, 3.0); + } + + public class C + { + public int Id { get; set; } + [BsonRepresentation(BsonType.Decimal128)] public decimal[] Decimals { get; set; } + public double[] Doubles { get; set; } + public float[] Floats { get; set; } + public int[] Ints { get; set; } + public long[] Longs { get; set; } + [BsonRepresentation(BsonType.Decimal128)] public decimal?[] NullableDecimals { get; set; } + public double?[] NullableDoubles { get; set; } + public float?[] NullableFloats { get; set; } + public int?[] NullableInts { get; set; } + public long?[] NullableLongs { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new() + { + Id = 1, + Decimals = [1.0M], + Doubles = [1.0], + Floats = [1.0F], + Ints = [1], + Longs = [1L], + NullableDecimals = [], + NullableDoubles = [], + NullableFloats = [], + NullableInts = [], + NullableLongs = [] + }, + new() + { + Id = 2, + Decimals = [1.0M, 2.0M], + Doubles = [1.0, 2.0], + Floats = [1.0F, 2.0F], + Ints = [1, 2], + Longs = [1L, 2L], + NullableDecimals = [null], + NullableDoubles = [null], + NullableFloats = [null], + NullableInts = [null], + NullableLongs = [null] + }, + new() + { + Id = 3, + Decimals = [1.0M, 2.0M, 3.0M], + Doubles = [1.0, 2.0, 3.0], + Floats = [1.0F, 2.0F, 3.0F], + Ints = [1, 2, 3], + Longs = [1L, 2L, 3L], + NullableDecimals = [null, 1.0M, 2.0M, 3.0M], + NullableDoubles = [null, 1.0, 2.0, 3.0], + NullableFloats = [null, 1.0F, 2.0F, 3.0F], + NullableInts = [null, 1, 2, 3], + NullableLongs = [null, 1L, 2L, 3L] + } + ]; + } + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslatorTests.cs index 59cdd9c6539..b34c3f77e3b 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/WindowMethodToAggregationExpressionTranslatorTests.cs @@ -1488,6 +1488,244 @@ public void Translate_should_return_expected_result_for_Max() } } + [Fact] + public void Translate_should_return_expected_result_for_Median_with_Decimal() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.DecimalField, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$DecimalField', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].ToDecimal().Should().Be(2.0M); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_Double() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.DoubleField, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$DoubleField', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_Int32() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.Int32Field, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$Int32Field', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_Int64() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.Int64Field, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$Int64Field', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_nullable_Decimal() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.NullableDecimalField, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$NullableDecimalField', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].ToDecimal().Should().Be(1.0M); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_nullable_Double() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.NullableDoubleField, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$NullableDoubleField', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_nullable_Int32() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.NullableInt32Field, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$NullableInt32Field', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_nullable_Int64() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.NullableInt64Field, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$NullableInt64Field', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_nullable_Single() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.NullableSingleField, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$NullableSingleField', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_Single() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Median(x => x.SingleField, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $median : { input : '$SingleField', method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Median_with_window() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields( + partitionBy: x => 1, + sortBy: Builders.Sort.Ascending(x => x.Id), + output: p => new { + Result = p.Median(x => x.Int32Field, DocumentsWindow.Create(-1, 1)) + }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] + { + "{ $setWindowFields : { partitionBy : 1, sortBy : { _id : 1 }, output : { Result : { $median : { input : '$Int32Field', method : 'approximate' }, window : { documents : [-1, 1] } } } } }" + }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + results[0]["Result"].AsDouble.Should().Be(1.0); + results[1]["Result"].AsDouble.Should().Be(2.0); + results[2]["Result"].AsDouble.Should().Be(2.0); + } + [Fact] public void Translate_should_return_expected_result_for_Min() { @@ -1507,6 +1745,292 @@ public void Translate_should_return_expected_result_for_Min() } } + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_Decimal() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.DecimalField, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$DecimalField', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].ToDecimal().Should().Be(2.0M); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_Double() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.DoubleField, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$DoubleField', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_Int32() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.Int32Field, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$Int32Field', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_Int64() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.Int64Field, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$Int64Field', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_nullable_Decimal() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.NullableDecimalField, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$NullableDecimalField', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].ToDecimal().Should().Be(1.0M); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_nullable_Double() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.NullableDoubleField, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$NullableDoubleField', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_nullable_Int32() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.NullableInt32Field, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$NullableInt32Field', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_nullable_Int64() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.NullableInt64Field, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$NullableInt64Field', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_nullable_Single() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.NullableSingleField, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$NullableSingleField', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(1.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_multiple_percentiles() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.Int32Field, new[] { 0.25, 0.75 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$Int32Field', p : [0.25, 0.75], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + var array = result["Result"].AsBsonArray; + array[0].AsDouble.Should().Be(1.0); + array[1].AsDouble.Should().Be(3.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_Single() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.SingleField, new[] { 0.5 }, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$SingleField', p : [0.5], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + result["Result"].AsBsonArray[0].AsDouble.Should().Be(2.0); + } + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_window() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + + var aggregate = collection.Aggregate() + .SetWindowFields( + partitionBy: x => 1, + sortBy: Builders.Sort.Ascending(x => x.Id), + output: p => new { + Result = p.Percentile(x => x.Int32Field, new[] { 0.5 }, DocumentsWindow.Create(-1, 1)) + }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] + { + "{ $setWindowFields : { partitionBy : 1, sortBy : { _id : 1 }, output : { Result : { $percentile : { input : '$Int32Field', p : [0.5], method : 'approximate' }, window : { documents : [-1, 1] } } } } }" + }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + results[0]["Result"].AsBsonArray[0].AsDouble.Should().Be(1.0); + results[1]["Result"].AsBsonArray[0].AsDouble.Should().Be(2.0); + results[2]["Result"].AsBsonArray[0].AsDouble.Should().Be(2.0); + } + + [Fact] + public void Translate_should_return_expected_result_for_Percentile_with_List_input() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var collection = Fixture.Collection; + var percentiles = new List { 0.25, 0.5, 0.75 }; + + var aggregate = collection.Aggregate() + .SetWindowFields(output: p => new { Result = p.Percentile(x => x.Int32Field, percentiles, null) }); + + var stages = Translate(collection, aggregate); + var expectedStages = new[] { "{ $setWindowFields : { output : { Result : { $percentile : { input : '$Int32Field', p : [0.25, 0.5, 0.75], method : 'approximate' } } } } }" }; + AssertStages(stages, expectedStages); + + var results = aggregate.ToList(); + foreach (var result in results) + { + var array = result["Result"].AsBsonArray; + array[0].AsDouble.Should().Be(1.0); + array[1].AsDouble.Should().Be(2.0); + array[2].AsDouble.Should().Be(3.0); + } + } + [Fact] public void Translate_should_return_expected_result_for_Push() { diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationWithLinq2Tests/Translators/AggregateGroupTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationWithLinq2Tests/Translators/AggregateGroupTranslatorTests.cs index 45bbf7067af..a8f7428079b 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationWithLinq2Tests/Translators/AggregateGroupTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3ImplementationWithLinq2Tests/Translators/AggregateGroupTranslatorTests.cs @@ -20,6 +20,7 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.Driver.Linq.Linq3Implementation.Ast; @@ -164,6 +165,66 @@ public void Should_translate_count_with_a_predicate() result.Value.Result.Should().Be(1); } + [Fact] + public void Should_translate_median_with_embedded_projector() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var result = Group(x => x.A, g => new { Result = g.Median(x=> x.C.E.F) }); + + AssertStages( + result.Stages, + "{ $group : { _id : '$A', __agg0 : { $median : { input : '$C.E.F', method : 'approximate' } } } }", + "{ $project : { Result : '$__agg0', _id : 0 } }"); + + result.Value.Result.Should().Be(111); + } + + [Fact] + public void Should_translate_median_with_selected_projector() + { + RequireServer.Check().Supports(Feature.MedianOperator); + + var result = Group(x => x.A, g => new { Result = g.Select(x => x.C.E.F).Median() }); + + AssertStages( + result.Stages, + "{ $group : { _id : '$A', __agg0 : { $median : { input : '$C.E.F', method : 'approximate' } } } }", + "{ $project : { Result : '$__agg0', _id : 0 } }"); + + result.Value.Result.Should().Be(111); + } + + [Fact] + public void Should_translate_percentile_with_embedded_projector() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var result = Group(x => x.A, g => new { Result = g.Percentile(x => x.C.E.F, new[] { 0.5 }) }); + + AssertStages( + result.Stages, + "{ $group : { _id : '$A', __agg0 : { $percentile : { input : '$C.E.F', p : [0.5], method : 'approximate' } } } }", + "{ $project : { Result : '$__agg0', _id : 0 } }"); + + result.Value.Result.Should().Equal(111.0); + } + + [Fact] + public void Should_translate_percentile_with_selected_projector() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var result = Group(x => x.A, g => new { Result = g.Select(x => x.C.E.F).Percentile(new[] { 0.5 }) }); + + AssertStages( + result.Stages, + "{ $group : { _id : '$A', __agg0 : { $percentile : { input : '$C.E.F', p : [0.5], method : 'approximate' } } } }", + "{ $project : { Result : '$__agg0', _id : 0 } }"); + + result.Value.Result.Should().Equal(111.0); + } + [Fact] public void Should_translate_where_with_a_predicate_and_count() { @@ -519,6 +580,40 @@ public void Should_translate_complex_selector() result.Value.Max.Should().Be(333); } + [Fact] + public void Should_translate_complex_selector_with_quantile_methods() + { + RequireServer.Check().Supports(Feature.PercentileOperator); + + var result = Group(x => x.A, g => new + { + Median = g.Median(x => x.C.E.F + x.C.E.H), + Percentile = g.Percentile(x => x.C.E.F + x.C.E.H, new[] { 0.95 }) + }); + + AssertStages( + result.Stages, + @" + { + $group : { + _id : '$A', + __agg0 : { $median : { input : { $add : ['$C.E.F', '$C.E.H'] }, method : 'approximate' } }, + __agg1 : { $percentile : { input : { $add : ['$C.E.F', '$C.E.H'] }, p : [0.95], method : 'approximate' } } + } + }", + @" + { + $project : { + Median : '$__agg0', + Percentile : '$__agg1', + _id : 0 + } + }"); + + result.Value.Median.Should().Be(333); + result.Value.Percentile.Should().Equal(333); + } + [Fact] public void Should_translate_aggregate_expressions_with_user_provided_serializer_if_possible() {