diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java index 342c13e91c4..280efb62439 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/ArrayExpression.java @@ -16,7 +16,6 @@ package com.mongodb.client.model.expressions; -import java.util.function.BinaryOperator; import java.util.function.Function; import static com.mongodb.client.model.expressions.Expressions.of; @@ -51,28 +50,51 @@ public interface ArrayExpression extends Expression { */ ArrayExpression map(Function in); + IntegerExpression size(); + + BooleanExpression any(Function predicate); + + BooleanExpression all(Function predicate); + + NumberExpression sum(Function mapper); + + NumberExpression multiply(Function mapper); + + T max(T other); + + T min(T other); + + ArrayExpression maxN(IntegerExpression n); + ArrayExpression minN(IntegerExpression n); + + StringExpression join(Function mapper); + + ArrayExpression concat(Function> mapper); + + ArrayExpression union(Function> mapper); + /** - * Performs a reduction on the elements of this array, using the provided - * identity value and an associative reducing function, and returns - * the reduced value. The initial value must be the identity value for the - * reducing function. + * user asserts that i is in bounds for the array * - * @param initialValue the identity for the reducing function - * @param in the associative reducing function - * @return the reduced value + * @param i + * @return */ - T reduce(T initialValue, BinaryOperator in); - - IntegerExpression size(); - T elementAt(IntegerExpression i); default T elementAt(final int i) { return this.elementAt(of(i)); } + /** + * user asserts that array is not empty + * @return + */ T first(); + /** + * user asserts that array is not empty + * @return + */ T last(); BooleanExpression contains(T contains); diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java index 833d9546040..00d2fb9b3d3 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/DocumentExpression.java @@ -17,7 +17,6 @@ package com.mongodb.client.model.expressions; import org.bson.conversions.Bson; -import org.bson.types.Decimal128; import java.time.Instant; @@ -47,12 +46,8 @@ default BooleanExpression getBoolean(final String fieldName, final boolean other NumberExpression getNumber(String fieldName, NumberExpression other); - default NumberExpression getNumber(final String fieldName, final double other) { - return getNumber(fieldName, of(other)); - } - - default NumberExpression getNumber(final String fieldName, final Decimal128 other) { - return getNumber(fieldName, of(other)); + default NumberExpression getNumber(final String fieldName, final Number other) { + return getNumber(fieldName, Expressions.numberToExpression(other)); } IntegerExpression getInteger(String fieldName); diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java index 7629def2c15..abb32529b62 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java @@ -18,6 +18,7 @@ import org.bson.BsonArray; import org.bson.BsonDocument; +import org.bson.BsonInt32; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; @@ -385,7 +386,12 @@ public ArrayExpression filter(final Function sort() { + return new MqlExpression<>((cr) -> astDoc("$sortArray", new BsonDocument() + .append("input", this.toBsonValue(cr)) + .append("sortBy", new BsonInt32(1)))); + } + public T reduce(final T initialValue, final BinaryOperator in) { T varThis = variable("$$this"); T varValue = variable("$$value"); @@ -395,6 +401,77 @@ public T reduce(final T initialValue, final BinaryOperator in) { .append("in", extractBsonValue(cr, in.apply(varValue, varThis))))); } + @Override + public BooleanExpression any(final Function predicate) { + MqlExpression array = (MqlExpression) this.map(predicate); + return array.reduce(of(false), (a, b) -> a.or(b)); + } + + @Override + public BooleanExpression all(final Function predicate) { + MqlExpression array = (MqlExpression) this.map(predicate); + return array.reduce(of(true), (a, b) -> a.and(b)); + } + + @SuppressWarnings("unchecked") + @Override + public NumberExpression sum(final Function mapper) { + // no sum that returns IntegerExpression, both have same erasure + MqlExpression array = (MqlExpression) this.map(mapper); + return array.reduce(of(0), (a, b) -> a.add(b)); + } + + @SuppressWarnings("unchecked") + @Override + public NumberExpression multiply(final Function mapper) { + MqlExpression array = (MqlExpression) this.map(mapper); + return array.reduce(of(0), (NumberExpression a, NumberExpression b) -> a.multiply(b)); + } + + @Override + public T max(final T other) { + return this.size().eq(of(0)).cond(other, this.maxN(of(1)).first()); + } + + @Override + public T min(final T other) { + return this.size().eq(of(0)).cond(other, this.minN(of(1)).first()); + } + + @Override + public ArrayExpression maxN(final IntegerExpression n) { + return newMqlExpression((CodecRegistry cr) -> astDoc("$maxN", new BsonDocument() + .append("input", extractBsonValue(cr, this)) + .append("n", extractBsonValue(cr, n)))); + } + + @Override + public ArrayExpression minN(final IntegerExpression n) { + return newMqlExpression((CodecRegistry cr) -> astDoc("$minN", new BsonDocument() + .append("input", extractBsonValue(cr, this)) + .append("n", extractBsonValue(cr, n)))); + } + + @Override + public StringExpression join(final Function mapper) { + MqlExpression array = (MqlExpression) this.map(mapper); + return array.reduce(of(""), (a, b) -> a.concat(b)); + } + + @SuppressWarnings("unchecked") + @Override + public ArrayExpression concat(final Function> mapper) { + MqlExpression> array = (MqlExpression>) this.map(mapper); + return array.reduce(Expressions.ofArray(), (a, b) -> a.concat(b)); + } + + @SuppressWarnings("unchecked") + @Override + public ArrayExpression union(final Function> mapper) { + MqlExpression> array = (MqlExpression>) this.map(mapper); + return array.reduce(Expressions.ofArray(), (a, b) -> a.union(b)); + } + @Override public IntegerExpression size() { return new MqlExpression<>(astWrapped("$size")); diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java index 69f3399da02..b622688b49f 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -37,7 +38,7 @@ import static com.mongodb.client.model.expressions.Expressions.ofStringArray; import static org.junit.jupiter.api.Assertions.assertThrows; -@SuppressWarnings({"ConstantConditions", "Convert2MethodRef"}) +@SuppressWarnings({"Convert2MethodRef"}) class ArrayExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#array-expression-operators // (Incomplete) @@ -128,40 +129,173 @@ public void mapTest() { } @Test - public void reduceTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/ + public void sortTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/ + ArrayExpression integerExpressionArrayExpression = ofIntegerArray(3, 1, 2); assertExpression( - Stream.of(true, true, false) - .reduce(false, (a, b) -> a || b), - arrayTTF.reduce(of(false), (a, b) -> a.or(b)), + Stream.of(3, 1, 2) + .sorted().collect(Collectors.toList()), sort(integerExpressionArrayExpression), // MQL: - "{'$reduce': {'input': [true, true, false], 'initialValue': false, 'in': {'$or': ['$$value', '$$this']}}}"); + "{'$sortArray': {'input': [3, 1, 2], 'sortBy': 1}}"); + } + + @SuppressWarnings("unchecked") + private static ArrayExpression sort(final ArrayExpression array) { + MqlExpression mqlArray = (MqlExpression) array; + return mqlArray.sort(); + } + + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/ + // reduce is implemented as each individual type of reduction (monoid) + // this prevents issues related to incorrect specification of identity values + + @Test + public void reduceAnyTest() { assertExpression( - Stream.of(true, true, false) - .reduce(true, (a, b) -> a && b), - arrayTTF.reduce(of(true), (a, b) -> a.and(b)), - // MQL: - "{'$reduce': {'input': [true, true, false], 'initialValue': true, 'in': {'$and': ['$$value', '$$this']}}}"); - // empty array + true, + arrayTTF.any(a -> a), + "{'$reduce': {'input': {'$map': {'input': [true, true, false], 'in': '$$this'}}, " + + "'initialValue': false, 'in': {'$or': ['$$value', '$$this']}}}"); assertExpression( - Stream.empty().reduce(true, (a, b) -> a && b), - ofBooleanArray().reduce(of(true), (a, b) -> a.and(b)), - // MQL: - "{'$reduce': {'input': [], 'initialValue': true, 'in': {'$and': ['$$value', '$$this']}}}"); - // constant result + false, + ofBooleanArray().any(a -> a)); + assertExpression( - Stream.of(true, true, false) - .reduce(true, (a, b) -> true), - arrayTTF.reduce(of(true), (a, b) -> of(true)), - // MQL: - "{'$reduce': {'input': [true, true, false], 'initialValue': true, 'in': true}}"); - // non-commutative + true, + ofIntegerArray(1, 2, 3).any(a -> a.eq(of(3)))); + assertExpression( + false, + ofIntegerArray(1, 2, 2).any(a -> a.eq(of(9)))); + } + + @Test + public void reduceAllTest() { + assertExpression( + false, + arrayTTF.all(a -> a), + "{'$reduce': {'input': {'$map': {'input': [true, true, false], 'in': '$$this'}}, " + + "'initialValue': true, 'in': {'$and': ['$$value', '$$this']}}}"); + assertExpression( + true, + ofBooleanArray().all(a -> a)); + + assertExpression( + true, + ofIntegerArray(1, 2, 3).all(a -> a.gt(of(0)))); + assertExpression( + false, + ofIntegerArray(1, 2, 2).all(a -> a.eq(of(2)))); + } + + @Test + public void reduceSumTest() { + assertExpression( + 6, + ofIntegerArray(1, 2, 3).sum(a -> a), + "{'$reduce': {'input': {'$map': {'input': [1, 2, 3], 'in': '$$this'}}, " + + "'initialValue': 0, 'in': {'$add': ['$$value', '$$this']}}}"); + // empty array: + assertExpression( + 0, + ofIntegerArray().sum(a -> a)); + } + + @Test + public void reduceMaxTest() { + assertExpression( + 3, + ofIntegerArray(1, 2, 3).max(of(9)), + "{'$cond': [{'$eq': [{'$size': [[1, 2, 3]]}, 0]}, 9, " + + "{'$first': [{'$maxN': {'input': [1, 2, 3], 'n': 1}}]}]}"); + assertExpression( + 9, + ofIntegerArray().max(of(9))); + } + + @Test + public void reduceMinTest() { + assertExpression( + 1, + ofIntegerArray(1, 2, 3).min(of(9)), + "{'$cond': [{'$eq': [{'$size': [[1, 2, 3]]}, 0]}, 9, " + + "{'$first': [{'$minN': {'input': [1, 2, 3], 'n': 1}}]}]}"); + assertExpression( + 9, + ofIntegerArray().min(of(9))); + } + + @Test + public void reduceMaxNTest() { + assertExpression( + Arrays.asList(3, 2), + ofIntegerArray(3, 1, 2).maxN(of(2))); + assertExpression( + Arrays.asList(), + ofIntegerArray().maxN(of(2))); + // N must be non-zero + assertThrows(MongoCommandException.class, () -> assertExpression( + Arrays.asList(), + ofIntegerArray(3, 2, 1).maxN(of(0)))); + } + + @Test + public void reduceMinNTest() { + assertExpression( + Arrays.asList(1, 2), + ofIntegerArray(3, 1, 2).minN(of(2))); + assertExpression( + Arrays.asList(), + ofIntegerArray().minN(of(2))); + // N must be non-zero + assertThrows(MongoCommandException.class, () -> assertExpression( + Arrays.asList(), + ofIntegerArray(3, 2, 1).minN(of(0)))); + } + + @Test + public void reduceJoinTest() { assertExpression( "abc", - ofStringArray("a", "b", "c").reduce(of(""), (a, b) -> a.concat(b)), + ofStringArray("a", "b", "c").join(a -> a), + "{'$reduce': {'input': {'$map': {'input': ['a', 'b', 'c'], 'in': '$$this'}}, " + + "'initialValue': '', 'in': {'$concat': ['$$value', '$$this']}}}"); + assertExpression( + "", + ofStringArray().join(a -> a)); + } + + @Test + public void reduceConcatTest() { + assertExpression( + Arrays.asList(1, 2, 3, 4), + ofArray(ofIntegerArray(1, 2), ofIntegerArray(3, 4)).concat(v -> v), + "{'$reduce': {'input': {'$map': {'input': [[1, 2], [3, 4]], 'in': '$$this'}}, " + + "'initialValue': [], " + + "'in': {'$concatArrays': ['$$value', '$$this']}}} "); + // empty: + ArrayExpression> expressionArrayExpression = ofArray(); + assertExpression( + Collections.emptyList(), + expressionArrayExpression.concat(a -> a)); + } + + @Test + public void reduceUnionTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ (40) + assertExpression( + Arrays.asList(1, 2, 3), + sort(ofArray(ofIntegerArray(1, 2), ofIntegerArray(1, 3)).union(v -> v)), // MQL: - "{'$reduce': {'input': ['a', 'b', 'c'], 'initialValue': '', 'in': {'$concat': ['$$value', '$$this']}}}"); + "{'$sortArray': {'input': {'$reduce': {'input': " + + "{'$map': {'input': [[1, 2], [1, 3]], 'in': '$$this'}}, " + + "'initialValue': [], 'in': {'$setUnion': ['$$value', '$$this']}}}, 'sortBy': 1}}"); + Function, ArrayExpression> f = a -> + a.map(v -> v.isBooleanOr(of(false)) + .cond(of(1), of(0))); + assertExpression( + Arrays.asList(0, 1), + ofArray(ofBooleanArray(true, false), ofBooleanArray(false)).union(f)); } @Test @@ -221,6 +355,12 @@ public void firstTest() { array123.first(), // MQL: "{'$first': [[1, 2, 3]]}"); + + assertExpression( + MISSING, + ofIntegerArray().first(), + // MQL: + "{'$first': [[]]}"); } @Test @@ -289,25 +429,24 @@ public void sliceTest() { } @Test - public void setUnionTest() { + public void unionTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ assertExpression( Arrays.asList(1, 2, 3), - array123.union(array123), + sort(array123.union(array123)), // MQL: - "{'$setUnion': [[1, 2, 3], [1, 2, 3]]}"); - + "{'$sortArray': {'input': {'$setUnion': [[1, 2, 3], [1, 2, 3]]}, 'sortBy': 1}}"); // mixed types: assertExpression( Arrays.asList(1, 2.0, 3), - // above is a set; in case of flakiness, below should `sort` (not implemented at time of test creation) - ofNumberArray(2.0).union(ofIntegerArray(1, 2, 3))); - // convenience + sort(ofNumberArray(2.0).union(ofIntegerArray(1, 2, 3)))); + } + + @Test + public void distinctTest() { assertExpression( Arrays.asList(1, 2, 3), - ofIntegerArray(1, 2, 1, 3, 3).distinct(), - // MQL: - "{'$setUnion': [[1, 2, 1, 3, 3]]}"); + sort(ofIntegerArray(1, 2, 1, 3, 3).distinct()), + "{'$sortArray': {'input': {'$setUnion': [[1, 2, 1, 3, 3]]}, 'sortBy': 1}}"); } - }