From 9348794fcc3fc56e69ce89964a149c8fd73dba9f Mon Sep 17 00:00:00 2001 From: Julia <5765049+sxhinzvc@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:41:13 -0400 Subject: [PATCH 1/3] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 366786fc6d..3bd4d68c6a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.2.0-SNAPSHOT + 4.2.x-4472-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 2de4b6b635..636e20872e 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.2.0-SNAPSHOT + 4.2.x-4472-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 41b81f9aa6..d28a8e16c5 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.2.0-SNAPSHOT + 4.2.x-4472-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index dc07f13ccc..91afc435ca 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.2.0-SNAPSHOT + 4.2.x-4472-SNAPSHOT ../pom.xml From 8c123844cd1fb2b5f9bb0f7e8986fa2f69caa4d6 Mon Sep 17 00:00:00 2001 From: Julia <5765049+sxhinzvc@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:00:52 -0400 Subject: [PATCH 2/3] Add support for $median aggregation operator. Closes #4472 --- .../aggregation/AccumulatorOperators.java | 84 +++++++++++++++++++ .../core/aggregation/ArithmeticOperators.java | 15 ++++ .../AccumulatorOperatorsUnitTests.java | 20 +++++ .../core/aggregation/AggregationTests.java | 43 ++++++++-- .../ArithmeticOperatorsUnitTests.java | 2 +- .../ProjectionOperationUnitTests.java | 16 ++++ 6 files changed, 173 insertions(+), 7 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java index a69555c4da..82375d27cb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java @@ -265,6 +265,16 @@ public Percentile percentile(Double... percentages) { return percentile.percentages(percentages); } + /** + * Creates new {@link AggregationExpression} that calculates the median of the associated numeric value expression. + * + * @return new instance of {@link Median}. + * @since 4.2 + */ + public Median median() { + return usesFieldRef() ? Median.medianOf(fieldReference) : Median.medianOf(expression); + } + private boolean usesFieldRef() { return fieldReference != null; } @@ -1082,4 +1092,78 @@ protected String getMongoMethod() { return "$percentile"; } } + + /** + * {@link AggregationExpression} for {@code $median}. + * + * @author Julia Lee + * @since 4.2 + */ + public static class Median extends AbstractAggregationExpression { + + private Median(Object value) { + super(value); + } + + /** + * Creates new {@link Median}. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link Median}. + */ + public static Median medianOf(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null"); + Map fields = new HashMap<>(); + fields.put("input", Fields.field(fieldReference)); + fields.put("method", "approximate"); + return new Median(fields); + } + + /** + * Creates new {@link Median}. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Median}. + */ + public static Median medianOf(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null"); + Map fields = new HashMap<>(); + fields.put("input", expression); + fields.put("method", "approximate"); + return new Median(fields); + } + + /** + * Creates new {@link Median} with all previously added inputs appending the given one.
+ * NOTE: Only possible in {@code $project} stage. + * + * @param fieldReference must not be {@literal null}. + * @return new instance of {@link Median}. + */ + public Median and(String fieldReference) { + + Assert.notNull(fieldReference, "FieldReference must not be null"); + return new Median(appendTo("input", Fields.field(fieldReference))); + } + + /** + * Creates new {@link Median} with all previously added inputs appending the given one.
+ * NOTE: Only possible in {@code $project} stage. + * + * @param expression must not be {@literal null}. + * @return new instance of {@link Median}. + */ + public Median and(AggregationExpression expression) { + + Assert.notNull(expression, "Expression must not be null"); + return new Median(appendTo("input", expression)); + } + + @Override + protected String getMongoMethod() { + return "$median"; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java index d985e3b7b4..8d665dade3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java @@ -24,6 +24,7 @@ import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovariancePop; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovarianceSamp; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Max; +import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Median; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Min; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Percentile; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.StdDevPop; @@ -31,6 +32,8 @@ import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits; +import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit; +import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -948,6 +951,18 @@ public Percentile percentile(Double... percentages) { return percentile.percentages(percentages); } + /** + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the + * numeric value. + * + * @return new instance of {@link Median}. + * @since 4.2 + */ + public Median median() { + return usesFieldRef() ? AccumulatorOperators.Median.medianOf(fieldReference) + : AccumulatorOperators.Median.medianOf(expression); + } + private boolean usesFieldRef() { return fieldReference != null; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java index a43b0f8620..3d7f770808 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java @@ -132,6 +132,26 @@ void rendersPercentileWithExpression() { .isEqualTo(Document.parse("{ $percentile: { input: [\"$scoreOne\", {\"$sum\": \"$scoreTwo\"}], method: \"approximate\", p: [0.1, 0.2] } }")); } + @Test // GH-4472 + void rendersMedianWithFieldReference() { + + assertThat(valueOf("score").median().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $median: { input: \"$score\", method: \"approximate\" } }")); + + assertThat(valueOf("score").median().and("scoreTwo").toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $median: { input: [\"$score\", \"$scoreTwo\"], method: \"approximate\" } }")); + } + + @Test // GH-4472 + void rendersMedianWithExpression() { + + assertThat(valueOf(Sum.sumOf("score")).median().toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $median: { input: {\"$sum\": \"$score\"}, method: \"approximate\" } }")); + + assertThat(valueOf("scoreOne").median().and(Sum.sumOf("scoreTwo")).toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo(Document.parse("{ $median: { input: [\"$scoreOne\", {\"$sum\": \"$scoreTwo\"}], method: \"approximate\" } }")); + } + static class Jedi { String name; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 5025d7fdce..ea4b218c45 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -1897,19 +1897,44 @@ void facetShouldCreateFacets() { @EnableIfMongoServerVersion(isGreaterThanEqual = "7.0") void percentileShouldBeAppliedCorrectly() { - mongoTemplate.insert(new DATAMONGO788(15, 16)); - mongoTemplate.insert(new DATAMONGO788(17, 18)); + DATAMONGO788 objectToSave = new DATAMONGO788(62, 81, 80); + DATAMONGO788 objectToSave2 = new DATAMONGO788(60, 83, 79); + + mongoTemplate.insert(objectToSave); + mongoTemplate.insert(objectToSave2); Aggregation agg = Aggregation.newAggregation( - project().and(ArithmeticOperators.valueOf("x").percentile(0.9).and("y")) - .as("ninetiethPercentile")); + project().and(ArithmeticOperators.valueOf("x").percentile(0.9, 0.4).and("y").and("xField")) + .as("percentileValues")); AggregationResults result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); // MongoDB server returns $percentile as an array of doubles List rawResults = (List) result.getRawResults().get("results"); - assertThat((List) rawResults.get(0).get("ninetiethPercentile")).containsExactly(16.0); - assertThat((List) rawResults.get(1).get("ninetiethPercentile")).containsExactly(18.0); + assertThat((List) rawResults.get(0).get("percentileValues")).containsExactly(81.0, 80.0); + assertThat((List) rawResults.get(1).get("percentileValues")).containsExactly(83.0, 79.0); + } + + @Test // GH-4472 + @EnableIfMongoServerVersion(isGreaterThanEqual = "7.0") + void medianShouldBeAppliedCorrectly() { + + DATAMONGO788 objectToSave = new DATAMONGO788(62, 81, 80); + DATAMONGO788 objectToSave2 = new DATAMONGO788(60, 83, 79); + + mongoTemplate.insert(objectToSave); + mongoTemplate.insert(objectToSave2); + + Aggregation agg = Aggregation.newAggregation( + project().and(ArithmeticOperators.valueOf("x").median().and("y").and("xField")) + .as("medianValue")); + + AggregationResults result = mongoTemplate.aggregate(agg, DATAMONGO788.class, Document.class); + + // MongoDB server returns $median a Double + List rawResults = (List) result.getRawResults().get("results"); + assertThat(rawResults.get(0).get("medianValue")).isEqualTo(80.0); + assertThat(rawResults.get(1).get("medianValue")).isEqualTo(79.0); } @Test // DATAMONGO-1986 @@ -2152,6 +2177,12 @@ public DATAMONGO788() {} this.y = y; this.yField = y; } + + public DATAMONGO788(int x, int y, int xField) { + this.x = x; + this.y = y; + this.xField = xField; + } } // DATAMONGO-806 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperatorsUnitTests.java index 6cc56af5a3..a506490d49 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperatorsUnitTests.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; /** - * Unit tests for {@link Round}. + * Unit tests for {@link ArithmeticOperators}. * * @author Christoph Strobl * @author Mark Paluch diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java index 87934ade1e..49218b2afc 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperationUnitTests.java @@ -2261,6 +2261,22 @@ void shouldRenderPercentileWithMultipleArgsAggregationExpression() { assertThat(agg).isEqualTo(Document.parse("{ $project: { scorePercentiles: { $percentile: { input: [\"$scoreOne\", \"$scoreTwo\"], method: \"approximate\", p: [0.4] } }} } }")); } + @Test // GH-4472 + void shouldRenderMedianAggregationExpressions() { + + Document singleArgAgg = project() + .and(ArithmeticOperators.valueOf("score").median()).as("medianValue") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(singleArgAgg).isEqualTo(Document.parse("{ $project: { medianValue: { $median: { input: \"$score\", method: \"approximate\" } }} } }")); + + Document multipleArgsAgg = project() + .and(ArithmeticOperators.valueOf("score").median().and("scoreTwo")).as("medianValue") + .toDocument(Aggregation.DEFAULT_CONTEXT); + + assertThat(multipleArgsAgg).isEqualTo(Document.parse("{ $project: { medianValue: { $median: { input: [\"$score\", \"$scoreTwo\"], method: \"approximate\" } }} } }")); + } + private static Document extractOperation(String field, Document fromProjectClause) { return (Document) fromProjectClause.get(field); } From 8eab315cbda98bc4156d839fdc0b6268456225de Mon Sep 17 00:00:00 2001 From: Julia <5765049+sxhinzvc@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:58:55 -0400 Subject: [PATCH 3/3] Polishing. Add $median to documentation. Add test to ensure adherence to TypedAggregationContext. Remove duplicate imports. See #4472 Original Pull Request: #4515 --- .../mongodb/core/aggregation/ArithmeticOperators.java | 2 -- .../core/aggregation/AccumulatorOperatorsUnitTests.java | 8 ++++++++ .../modules/ROOT/pages/mongodb/aggregation-framework.adoc | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java index 8d665dade3..7960920cc4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java @@ -32,8 +32,6 @@ import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits; -import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit; -import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java index 3d7f770808..225811d76b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperatorsUnitTests.java @@ -152,6 +152,14 @@ void rendersMedianWithExpression() { .isEqualTo(Document.parse("{ $median: { input: [\"$scoreOne\", {\"$sum\": \"$scoreTwo\"}], method: \"approximate\" } }")); } + @Test // GH-4472 + void rendersMedianCorrectlyWithTypedAggregationContext() { + + assertThat(valueOf("midichlorianCount").median() + .toDocument(TestAggregationContext.contextFor(Jedi.class))) + .isEqualTo(Document.parse("{ $median: { input: \"$force\", method: \"approximate\" } }")); + } + static class Jedi { String name; diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aggregation-framework.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aggregation-framework.adoc index 18cb70d4a6..eb50c1f06b 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/aggregation-framework.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/aggregation-framework.adoc @@ -112,7 +112,7 @@ At the time of this writing, we provide support for the following Aggregation Op | `setEquals`, `setIntersection`, `setUnion`, `setDifference`, `setIsSubset`, `anyElementTrue`, `allElementsTrue` | Group/Accumulator Aggregation Operators -| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `firstN`, `last`, `lastN` `max`, `maxN`, `min`, `minN`, `avg`, `push`, `sum`, `top`, `topN`, `count` (+++*+++), `percentile`, `stdDevPop`, `stdDevSamp` +| `addToSet`, `bottom`, `bottomN`, `covariancePop`, `covarianceSamp`, `expMovingAvg`, `first`, `firstN`, `last`, `lastN` `max`, `maxN`, `min`, `minN`, `avg`, `push`, `sum`, `top`, `topN`, `count` (+++*+++), `median`, `percentile`, `stdDevPop`, `stdDevSamp` | Arithmetic Aggregation Operators | `abs`, `acos`, `acosh`, `add` (+++*+++ via `plus`), `asin`, `asin`, `atan`, `atan2`, `atanh`, `ceil`, `cos`, `cosh`, `derivative`, `divide`, `exp`, `floor`, `integral`, `ln`, `log`, `log10`, `mod`, `multiply`, `pow`, `round`, `sqrt`, `subtract` (+++*+++ via `minus`), `sin`, `sinh`, `tan`, `tanh`, `trunc`