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 99f7da587fa..342c13e91c4 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 @@ -77,7 +77,7 @@ default T elementAt(final int i) { BooleanExpression contains(T contains); - ArrayExpression concat(ArrayExpression array); + ArrayExpression concat(ArrayExpression array); ArrayExpression slice(IntegerExpression start, IntegerExpression length); @@ -85,7 +85,7 @@ default ArrayExpression slice(final int start, final int length) { return this.slice(of(start), of(length)); } - ArrayExpression union(ArrayExpression set); + ArrayExpression union(ArrayExpression set); ArrayExpression distinct(); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java index dd1b2ae5324..04eb6b00ea5 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/DateExpression.java @@ -31,7 +31,6 @@ public interface DateExpression extends Expression { IntegerExpression week(StringExpression timezone); IntegerExpression millisecond(StringExpression timezone); - StringExpression dateToString(); - StringExpression dateToString(StringExpression timezone, StringExpression format); + StringExpression asString(StringExpression timezone, StringExpression format); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java b/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java index ac0b9dcbbef..1ef5e6460ef 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/Expression.java @@ -97,4 +97,17 @@ public interface Expression { * @return true if less than or equal to, false otherwise */ BooleanExpression lte(Expression lte); + + /** + * also checks for nulls + * @param or + * @return + */ + BooleanExpression isBooleanOr(BooleanExpression or); + NumberExpression isNumberOr(NumberExpression or); + StringExpression isStringOr(StringExpression or); + DateExpression isDateOr(DateExpression or); + ArrayExpression isArrayOr(ArrayExpression or); + T isDocumentOr(T or); + StringExpression asString(); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/Expressions.java b/driver-core/src/main/com/mongodb/client/model/expressions/Expressions.java index 28987a8dc01..511984ec6c3 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/Expressions.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/Expressions.java @@ -184,7 +184,7 @@ public static ArrayExpression ofArray(final T... array }); } - public static DocumentExpression ofDocument(final Bson document) { + public static DocumentExpression of(final Bson document) { Assertions.notNull("document", document); // All documents are wrapped in a $literal. If we don't wrap, we need to // check for empty documents and documents that are actually expressions @@ -193,7 +193,9 @@ public static DocumentExpression ofDocument(final Bson document) { document.toBsonDocument(BsonDocument.class, cr)))); } - public static R ofNull() { + public static Expression ofNull() { + // There is no specific expression type corresponding to Null, + // and Null is not a value in any other expression type. return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonNull())) .assertImplementsAllExpressions(); } 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 370f3ce7040..e67044a7315 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 @@ -26,6 +26,9 @@ import java.util.function.BinaryOperator; import java.util.function.Function; +import static com.mongodb.client.model.expressions.Expressions.of; +import static com.mongodb.client.model.expressions.Expressions.ofStringArray; + final class MqlExpression implements Expression, BooleanExpression, IntegerExpression, NumberExpression, StringExpression, DateExpression, DocumentExpression, ArrayExpression { @@ -61,6 +64,12 @@ private Function ast(final String name) { return (cr) -> new AstPlaceholder(new BsonDocument(name, this.toBsonValue(cr))); } + // in cases where we must wrap the first argument in an array + private Function astWrapped(final String name) { + return (cr) -> new AstPlaceholder(new BsonDocument(name, + new BsonArray(Collections.singletonList(this.toBsonValue(cr))))); + } + private Function ast(final String name, final Expression param1) { return (cr) -> { BsonArray value = new BsonArray(); @@ -161,6 +170,80 @@ public BooleanExpression lte(final Expression lte) { return new MqlExpression<>(ast("$lte", lte)); } + public BooleanExpression isBoolean() { + return new MqlExpression<>(ast("$type")).eq(of("bool")); + } + + @Override + public BooleanExpression isBooleanOr(final BooleanExpression or) { + return this.isBoolean().cond(this, or); + } + + public BooleanExpression isNumber() { + return new MqlExpression<>(astWrapped("$isNumber")); + } + + @Override + public NumberExpression isNumberOr(final NumberExpression or) { + return this.isNumber().cond(this, or); + } + + public BooleanExpression isString() { + return new MqlExpression<>(ast("$type")).eq(of("string")); + } + + @Override + public StringExpression isStringOr(final StringExpression or) { + return this.isString().cond(this, or); + } + + public BooleanExpression isDate() { + return ofStringArray("date").contains(new MqlExpression<>(ast("$type"))); + } + + @Override + public DateExpression isDateOr(final DateExpression or) { + return this.isDate().cond(this, or); + } + + public BooleanExpression isArray() { + return new MqlExpression<>(astWrapped("$isArray")); + } + + @SuppressWarnings("unchecked") // TODO + @Override + public ArrayExpression isArrayOr(final ArrayExpression or) { + // TODO it seems that ArrEx does not make sense here + return (ArrayExpression) this.isArray().cond(this.assertImplementsAllExpressions(), or); + } + + public BooleanExpression isDocument() { + return new MqlExpression<>(ast("$type")).eq(of("object")); + } + + @Override + public R isDocumentOr(final R or) { + return this.isDocument().cond(this.assertImplementsAllExpressions(), or); + } + + @Override + public StringExpression asString() { + return new MqlExpression<>(astWrapped("$toString")); + } + + private Function convertInternal(final String to, final Expression orElse) { + return (cr) -> astDoc("$convert", new BsonDocument() + .append("input", this.fn.apply(cr).bsonValue) + .append("onError", extractBsonValue(cr, orElse)) + .append("to", new BsonString(to))); + } + + @Override + public IntegerExpression parseInteger() { + Expression asLong = new MqlExpression<>(ast("$toLong")); + return new MqlExpression<>(convertInternal("int", asLong)); + } + /** @see ArrayExpression */ @Override @@ -191,10 +274,7 @@ public T reduce(final T initialValue, final BinaryOperator in) { @Override public IntegerExpression size() { - return new MqlExpression<>( - (cr) -> new AstPlaceholder(new BsonDocument("$size", - // must wrap the first argument in a list - new BsonArray(Collections.singletonList(this.toBsonValue(cr)))))); + return new MqlExpression<>(astWrapped("$size")); } @Override @@ -205,19 +285,13 @@ public T elementAt(final IntegerExpression at) { @Override public T first() { - return new MqlExpression<>( - (cr) -> new AstPlaceholder(new BsonDocument("$first", - // must wrap the first argument in a list - new BsonArray(Collections.singletonList(this.toBsonValue(cr)))))) + return new MqlExpression<>(astWrapped("$first")) .assertImplementsAllExpressions(); } @Override public T last() { - return new MqlExpression<>( - (cr) -> new AstPlaceholder(new BsonDocument("$last", - // must wrap the first argument in a list - new BsonArray(Collections.singletonList(this.toBsonValue(cr)))))) + return new MqlExpression<>(astWrapped("$last")) .assertImplementsAllExpressions(); } @@ -233,7 +307,7 @@ public BooleanExpression contains(final T item) { } @Override - public ArrayExpression concat(final ArrayExpression array) { + public ArrayExpression concat(final ArrayExpression array) { return new MqlExpression<>(ast("$concatArrays", array)) .assertImplementsAllExpressions(); } @@ -245,17 +319,14 @@ public ArrayExpression slice(final IntegerExpression start, final IntegerExpr } @Override - public ArrayExpression union(final ArrayExpression set) { + public ArrayExpression union(final ArrayExpression set) { return new MqlExpression<>(ast("$setUnion", set)) .assertImplementsAllExpressions(); } @Override public ArrayExpression distinct() { - return new MqlExpression<>( - (cr) -> new AstPlaceholder(new BsonDocument("$setUnion", - // must wrap the first argument in a list - new BsonArray(Collections.singletonList(this.toBsonValue(cr)))))); + return new MqlExpression<>(astWrapped("$setUnion")); } @@ -307,6 +378,11 @@ public IntegerExpression abs() { return newMqlExpression(ast("$abs")); } + @Override + public DateExpression millisecondsToDate() { + return newMqlExpression(ast("$toDate")); + } + @Override public NumberExpression subtract(final NumberExpression n) { return new MqlExpression<>(ast("$subtract", n)); @@ -391,19 +467,34 @@ public IntegerExpression millisecond(final StringExpression timezone) { } @Override - public StringExpression dateToString() { + public StringExpression asString(final StringExpression timezone, final StringExpression format) { return newMqlExpression((cr) -> astDoc("$dateToString", new BsonDocument() - .append("date", this.toBsonValue(cr)))); + .append("date", this.toBsonValue(cr)) + .append("format", extractBsonValue(cr, format)) + .append("timezone", extractBsonValue(cr, timezone)))); } @Override - public StringExpression dateToString(final StringExpression timezone, final StringExpression format) { - return newMqlExpression((cr) -> astDoc("$dateToString", new BsonDocument() - .append("date", this.toBsonValue(cr)) + public DateExpression parseDate(final StringExpression timezone, final StringExpression format) { + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)) .append("format", extractBsonValue(cr, format)) .append("timezone", extractBsonValue(cr, timezone)))); } + @Override + public DateExpression parseDate(final StringExpression format) { + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)) + .append("format", extractBsonValue(cr, format)))); + } + + @Override + public DateExpression parseDate() { + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)))); + } + /** @see StringExpression */ @Override diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java index bbef9f5768f..f08b9a57d00 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/NumberExpression.java @@ -54,4 +54,6 @@ default NumberExpression subtract(final Number subtract) { NumberExpression round(IntegerExpression place); NumberExpression abs(); + + DateExpression millisecondsToDate(); } diff --git a/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java index a0812878137..c5cc7a8eb72 100644 --- a/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java +++ b/driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java @@ -35,13 +35,21 @@ public interface StringExpression extends Expression { StringExpression substr(IntegerExpression start, IntegerExpression length); - default StringExpression substr(int start, int length) { + default StringExpression substr(final int start, final int length) { return this.substr(of(start), of(length)); } StringExpression substrBytes(IntegerExpression start, IntegerExpression length); - default StringExpression substrBytes(int start, int length) { + default StringExpression substrBytes(final int start, final int length) { return this.substrBytes(of(start), of(length)); } + + IntegerExpression parseInteger(); + + DateExpression parseDate(); + + DateExpression parseDate(StringExpression format); + + DateExpression parseDate(StringExpression timezone, StringExpression format); } diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java index 4b966294027..9a4899a0f76 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java @@ -44,6 +44,11 @@ public abstract class AbstractExpressionsFunctionalTest extends OperationTest { + /** + * Java stand-in for the "missing" value. + */ + public static final Object MISSING = new Object(); + @BeforeEach public void setUp() { getCollectionHelper().drop(); @@ -54,7 +59,7 @@ public void tearDown() { getCollectionHelper().drop(); } - protected void assertExpression(final Object expected, final Expression expression) { + protected void assertExpression(@Nullable final Object expected, final Expression expression) { assertExpression(expected, expression, null); } @@ -74,6 +79,10 @@ protected void assertExpression(@Nullable final Object expected, final Expressio private void assertEval(@Nullable final Object expected, final Expression toEvaluate) { BsonValue evaluated = evaluate(toEvaluate); + if (expected == MISSING && evaluated == null) { + // if the "val" field was removed by "missing", then evaluated is null + return; + } BsonValue expected1 = toBsonValue(expected); assertEquals(expected1, evaluated); } @@ -85,6 +94,7 @@ protected BsonValue toBsonValue(@Nullable final Object value) { return new Document("val", value).toBsonDocument().get("val"); } + @Nullable protected BsonValue evaluate(final Expression toEvaluate) { Bson addFieldsStage = addFields(new Field<>("val", toEvaluate)); List stages = new ArrayList<>(); diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArithmeticExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArithmeticExpressionsFunctionalTest.java index 4e156c86516..a1e289270d8 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArithmeticExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArithmeticExpressionsFunctionalTest.java @@ -89,15 +89,27 @@ public void multiplyTest() { assertExpression(2, of(1).multiply(2)); } + @SuppressWarnings("PointlessArithmeticExpression") @Test public void divideTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/divide/ + assertExpression( + 2.0 / 1.0, + of(2.0).divide(of(1.0)), + "{'$divide': [2.0, 1.0]}"); + + // division always converts to a double: + assertExpression( + 2.0, // not: 2 / 1 + of(2).divide(of(1)), + "{'$divide': [2, 1]}"); + + // this means that unlike Java's 1/2==0, dividing any underlying + // BSON number type always yields an equal result: assertExpression( 1.0 / 2.0, of(1.0).divide(of(2.0)), "{'$divide': [1.0, 2.0]}"); - // unlike Java's 1/2==0, dividing any type of numbers always yields an - // equal result, in this case represented using a double. assertExpression( 0.5, of(1).divide(of(2)), @@ -117,6 +129,11 @@ public void divideTest() { assertExpression( Decimal128.parse("2.524218750000"), of(3.231).divide(of(Decimal128.parse("1.28")))); + // this is not simply because the Java literal used has no corresponding + // double value - it is the same value as-written: + assertEquals("3.231", "" + 3.231); + assertEquals("1.28", "" + 1.28); + // convenience assertExpression(0.5, of(1.0).divide(2.0)); @@ -139,6 +156,17 @@ public void addTest() { of(2.0).add(of(2)), "{'$add': [2.0, 2]}"); + // overflows into a supported underlying type + assertExpression( + Integer.MAX_VALUE + 2L, + of(Integer.MAX_VALUE).add(of(2))); + assertExpression( + Long.MAX_VALUE + 2.0, + of(Long.MAX_VALUE).add(of(2))); + assertExpression( + Double.POSITIVE_INFINITY, + of(Double.MAX_VALUE).add(of(Double.MAX_VALUE))); + // convenience assertExpression(3.0, of(1.0).add(2.0)); assertExpression(3L, of(1).add(2L)); @@ -183,7 +211,7 @@ public void maxTest() { @Test public void minTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/ (63) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/ IntegerExpression actual = of(-2).min(of(2)); assertExpression( Math.min(-2, 2), @@ -225,11 +253,21 @@ public void roundTest() { 600.0, of(555.555).round(of(-2)), "{'$round': [555.555, -2]} "); + // underlying type rounds to same underlying type + assertExpression( + 5L, + of(5L).round()); + assertExpression( + 5.0, + of(5.0).round()); + assertExpression( + Decimal128.parse("1234"), + of(Decimal128.parse("1234.2")).round()); } @Test public void absTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/ (?) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/ assertExpression( Math.abs(-2.0), of(-2.0).abs(), 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 e38058551e7..24145443c23 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 @@ -16,6 +16,7 @@ package com.mongodb.client.model.expressions; +import com.mongodb.MongoCommandException; import org.bson.types.Decimal128; import org.junit.jupiter.api.Test; @@ -33,6 +34,7 @@ import static com.mongodb.client.model.expressions.Expressions.ofIntegerArray; import static com.mongodb.client.model.expressions.Expressions.ofNumberArray; import static com.mongodb.client.model.expressions.Expressions.ofStringArray; +import static org.junit.jupiter.api.Assertions.assertThrows; @SuppressWarnings({"ConstantConditions", "Convert2MethodRef"}) class ArrayExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { @@ -177,17 +179,27 @@ public void elementAtTest() { array123.elementAt((IntegerExpression) of(0.0)), // MQL: "{'$arrayElemAt': [[1, 2, 3], 0.0]}"); - + // negatives assertExpression( Arrays.asList(1, 2, 3).get(3 - 1), array123.elementAt(-1)); + // underlying long + assertExpression( + 2, + array123.elementAt(of(1L))); assertExpression( - true, - ofRem().eq(array123.elementAt(99))); + MISSING, + array123.elementAt(99)); + assertExpression( - true, - ofRem().eq(array123.elementAt(-99))); + MISSING, + array123.elementAt(-99)); + + // long values are considered entirely out of bounds; server error + assertThrows(MongoCommandException.class, () -> assertExpression( + MISSING, + array123.elementAt(of(Long.MAX_VALUE)))); } @Test @@ -209,6 +221,12 @@ public void lastTest() { array123.last(), // MQL: "{'$last': [[1, 2, 3]]}"); + + assertExpression( + MISSING, + ofIntegerArray().last(), + // MQL: + "{'$last': [[]]}"); } @Test @@ -228,9 +246,13 @@ public void concatTest() { assertExpression( Stream.concat(Stream.of(1, 2, 3), Stream.of(1, 2, 3)) .collect(Collectors.toList()), - array123.concat(array123), + ofIntegerArray(1, 2, 3).concat(ofIntegerArray(1, 2, 3)), // MQL: "{'$concatArrays': [[1, 2, 3], [1, 2, 3]]}"); + // mixed types: + assertExpression( + Arrays.asList(1.0, 1, 2, 3), + ofNumberArray(1.0).concat(ofIntegerArray(1, 2, 3))); } @Test @@ -258,12 +280,18 @@ public void sliceTest() { @Test public void setUnionTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ (40) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ assertExpression( Arrays.asList(1, 2, 3), array123.union(array123), // MQL: "{'$setUnion': [[1, 2, 3], [1, 2, 3]]}"); + + // 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 assertExpression( Arrays.asList(1, 2, 3), diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java index ad1ca67a723..0587a6d7871 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java @@ -27,7 +27,6 @@ import static com.mongodb.client.model.expressions.Expressions.of; import static com.mongodb.client.model.expressions.Expressions.ofBooleanArray; -import static com.mongodb.client.model.expressions.Expressions.ofDocument; import static com.mongodb.client.model.expressions.Expressions.ofIntegerArray; import static com.mongodb.client.model.expressions.Expressions.ofNull; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; @@ -47,12 +46,12 @@ class ComparisonExpressionsFunctionalTest extends AbstractExpressionsFunctionalT of(1), of(""), of("str"), - ofDocument(BsonDocument.parse("{}")), - ofDocument(BsonDocument.parse("{a: 1}")), - ofDocument(BsonDocument.parse("{a: 2}")), - ofDocument(BsonDocument.parse("{a: 2, b: 1}")), - ofDocument(BsonDocument.parse("{b: 1, a: 2}")), - ofDocument(BsonDocument.parse("{'':''}")), + of(BsonDocument.parse("{}")), + of(BsonDocument.parse("{a: 1}")), + of(BsonDocument.parse("{a: 2}")), + of(BsonDocument.parse("{a: 2, b: 1}")), + of(BsonDocument.parse("{b: 1, a: 2}")), + of(BsonDocument.parse("{'':''}")), ofIntegerArray(0), ofIntegerArray(1), ofBooleanArray(true), @@ -70,7 +69,7 @@ public void eqTest() { "{'$eq': [1, 2]}"); assertExpression( false, - ofDocument(BsonDocument.parse("{}")).eq(ofIntegerArray()), + of(BsonDocument.parse("{}")).eq(ofIntegerArray()), "{'$eq': [{'$literal': {}}, []]}"); // numbers are equal, even though of different types diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/DateExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/DateExpressionsFunctionalTest.java index 73cec9c5198..d801297bbbb 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/DateExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/DateExpressionsFunctionalTest.java @@ -25,8 +25,9 @@ import java.time.temporal.ChronoField; import static com.mongodb.client.model.expressions.Expressions.of; -import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static org.junit.jupiter.api.Assertions.assertThrows; +@SuppressWarnings("ConstantConditions") class DateExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#date-expression-operators @@ -41,6 +42,7 @@ public void literalsTest() { instant, date, "{'$date': '2007-12-03T10:15:30.005Z'}"); + assertThrows(IllegalArgumentException.class, () -> of((Instant) null)); } @Test @@ -133,25 +135,4 @@ public void millisecondTest() { "{'$millisecond': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, 'timezone': 'UTC'}}"); } - @Test - public void dateToStringTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ - assertExpression( - instant.toString(), - date.dateToString(), - "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}}}"); - // with parameters - assertExpression( - utcDateTime.withZoneSameInstant(ZoneId.of("America/New_York")).format(ISO_LOCAL_DATE_TIME), - date.dateToString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%L")), - "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " - + "'format': '%Y-%m-%dT%H:%M:%S.%L', " - + "'timezone': 'America/New_York'}}"); - assertExpression( - utcDateTime.withZoneSameInstant(ZoneId.of("+04:30")).format(ISO_LOCAL_DATE_TIME), - date.dateToString(of("+04:30"), of("%Y-%m-%dT%H:%M:%S.%L")), - "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " - + "'format': '%Y-%m-%dT%H:%M:%S.%L', " - + "'timezone': '+04:30'}}"); - } } diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/StringExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/StringExpressionsFunctionalTest.java index b4092963c9c..5ba68f3b8b2 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/expressions/StringExpressionsFunctionalTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/StringExpressionsFunctionalTest.java @@ -60,7 +60,7 @@ public void toLowerTest() { @Test public void toUpperTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/ (?) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/ assertExpression( "abc".toUpperCase(), of("abc").toUpper(), @@ -69,7 +69,7 @@ public void toUpperTest() { @Test public void strLenTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/ (?) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/ assertExpression( "abc".codePointCount(0, 3), of("abc").strLen(), @@ -92,7 +92,7 @@ public void strLenTest() { @Test public void strLenBytesTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/ (?) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/ assertExpression( "abc".getBytes(StandardCharsets.UTF_8).length, of("abc").strLenBytes(), @@ -124,7 +124,7 @@ public void strLenBytesTest() { @Test public void substrTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substr/ - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/ (?) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/ // substr is deprecated, an alias for bytes assertExpression( "abc".substring(1, 1 + 1), @@ -149,7 +149,7 @@ public void substrTest() { @Test public void substrBytesTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/ (?) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/ assertExpression( "b", of("abc").substrBytes(of(1), of(1)), diff --git a/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java new file mode 100644 index 00000000000..1f9daf28a3a --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2008-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. + */ + +package com.mongodb.client.model.expressions; + +import com.mongodb.MongoCommandException; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.types.Decimal128; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Arrays; + +import static com.mongodb.client.model.expressions.Expressions.of; +import static com.mongodb.client.model.expressions.Expressions.ofIntegerArray; +import static com.mongodb.client.model.expressions.Expressions.ofNull; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TypeExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/#type-expression-operators + + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/ + // type is not implemented directly; instead, similar checks done via switch + + @Test + public void isBooleanOrTest() { + assertExpression( + true, + of(true).isBooleanOr(of(false)), + "{'$cond': [{'$eq': [{'$type': true}, 'bool']}, true, false]}"); + // non-boolean: + assertExpression(false, ofIntegerArray(1).isBooleanOr(of(false))); + assertExpression(false, ofNull().isBooleanOr(of(false))); + } + + @Test + public void isNumberOrTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/ + assertExpression(1, of(1).isNumberOr(of(99)), "{'$cond': [{'$isNumber': [1]}, 1, 99]}"); + // other numeric values: + assertExpression(1L, of(1L).isNumberOr(of(99))); + assertExpression(1.0, of(1.0).isNumberOr(of(99))); + assertExpression(Decimal128.parse("1"), of(Decimal128.parse("1")).isNumberOr(of(99))); + // non-numeric: + assertExpression(99, ofIntegerArray(1).isNumberOr(of(99))); + assertExpression(99, ofNull().isNumberOr(of(99))); + } + + @Test + public void isStringOrTest() { + assertExpression( + "abc", + of("abc").isStringOr(of("or")), + "{'$cond': [{'$eq': [{'$type': 'abc'}, 'string']}, 'abc', 'or']}"); + // non-string: + assertExpression("or", ofIntegerArray(1).isStringOr(of("or"))); + assertExpression("or", ofNull().isStringOr(of("or"))); + } + + @Test + public void isDateOrTest() { + Instant date = Instant.parse("2007-12-03T10:15:30.005Z"); + assertExpression( + date, + of(date).isDateOr(of(date.plusMillis(10))), + "{'$cond': [{'$in': [{'$type': {'$date': '2007-12-03T10:15:30.005Z'}}, ['date']]}, " + + "{'$date': '2007-12-03T10:15:30.005Z'}, {'$date': '2007-12-03T10:15:30.015Z'}]}"); + // non-date: + assertExpression(date, ofIntegerArray(1).isDateOr(of(date))); + assertExpression(date, ofNull().isDateOr(of(date))); + } + + @Test + public void isArrayOrTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/ + assertExpression( + Arrays.asList(1, 2), + ofIntegerArray(1, 2).isArrayOr(ofIntegerArray(99)), + "{'$cond': [{'$isArray': [[1, 2]]}, [1, 2], [99]]}"); + // non-array: + assertExpression(Arrays.asList(1, 2), of(true).isArrayOr(ofIntegerArray(1, 2))); + assertExpression(Arrays.asList(1, 2), ofNull().isArrayOr(ofIntegerArray(1, 2))); + } + + @Test + public void isDocumentOrTest() { + BsonDocument doc = BsonDocument.parse("{a: 1}"); + assertExpression(doc, + of(doc).isDocumentOr(of(BsonDocument.parse("{b: 2}"))), + "{'$cond': [{'$eq': [{'$type': {'$literal': {'a': 1}}}, 'object']}, " + + "{'$literal': {'a': 1}}, {'$literal': {'b': 2}}]}"); + // non-document: + assertExpression(doc, ofIntegerArray(1).isDocumentOr(of(doc))); + assertExpression(doc, ofNull().isDocumentOr(of(doc))); + } + + // conversions + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/ + // Convert is not implemented: too dynamic, conversions should be explicit. + + @Test + public void asStringTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/ + // asString, since toString conflicts + assertExpression("false", of(false).asString(), "{'$toString': [false]}"); + + assertExpression("1", of(1).asString()); + assertExpression("1", of(1L).asString()); + assertExpression("1", of(1.0).asString()); + assertExpression("1.0", of(Decimal128.parse("1.0")).asString()); + + assertExpression("abc", of("abc").asString()); + + // this is equivalent to $dateToString + assertExpression("1970-01-01T00:00:00.123Z", of(Instant.ofEpochMilli(123)).asString()); + + // Arrays and documents are not (yet) supported: + assertThrows(MongoCommandException.class, () -> + assertExpression("[]", ofIntegerArray(1, 2).asString())); + assertThrows(MongoCommandException.class, () -> + assertExpression("[1, 2]", ofIntegerArray(1, 2).asString())); + assertThrows(MongoCommandException.class, () -> + assertExpression("{a: 1}", of(Document.parse("{a: 1}")).asString())); + } + + @Test + public void dateAsStringTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ + final Instant instant = Instant.parse("2007-12-03T10:15:30.005Z"); + DateExpression date = of(instant); + ZonedDateTime utcDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of(ZoneOffset.UTC.getId())); + assertExpression( + "2007-12-03T10:15:30.005Z", + of(instant).asString(), + "{'$toString': [{'$date': '2007-12-03T10:15:30.005Z'}]}"); + // with parameters + assertExpression( + utcDateTime.withZoneSameInstant(ZoneId.of("America/New_York")).format(ISO_LOCAL_DATE_TIME), + date.asString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%L")), + "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + + "'format': '%Y-%m-%dT%H:%M:%S.%L', " + + "'timezone': 'America/New_York'}}"); + assertExpression( + utcDateTime.withZoneSameInstant(ZoneId.of("+04:30")).format(ISO_LOCAL_DATE_TIME), + date.asString(of("+04:30"), of("%Y-%m-%dT%H:%M:%S.%L")), + "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + + "'format': '%Y-%m-%dT%H:%M:%S.%L', " + + "'timezone': '+04:30'}}"); + // Olson Timezone Identifier is changed to UTC offset: + assertExpression( + "2007-12-03T05:15:30.005-0500", + of(instant).asString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%L%z"))); + } + + // parse string + + @Test + public void parseDateTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ + String dateString = "2007-12-03T10:15:30.005Z"; + assertExpression( + Instant.parse(dateString), + of(dateString).parseDate(), + "{'$dateFromString': {'dateString': '2007-12-03T10:15:30.005Z'}}"); + + + // throws: "cannot pass in a date/time string with GMT offset together with a timezone argument" + assertThrows(MongoCommandException.class, () -> + assertExpression(1, of("2007-12-03T10:15:30.005+01:00") + .parseDate(of("+01:00"), of("%Y-%m-%dT%H:%M:%S.%L%z")) + .asString())); + // therefore, to parse date strings containing UTC offsets, we need: + assertExpression( + Instant.parse("2007-12-03T09:15:30.005Z"), + of("2007-12-03T10:15:30.005+01:00") + .parseDate(of("%Y-%m-%dT%H:%M:%S.%L%z")), + "{'$dateFromString': {'dateString': '2007-12-03T10:15:30.005+01:00', " + + "'format': '%Y-%m-%dT%H:%M:%S.%L%z'}}"); + } + + @Test + public void parseIntegerTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/ + assertExpression( + 1234, + of("1234").parseInteger(), + "{'$convert': {'input': '1234', 'onError': {'$toLong': '1234'}, 'to': 'int'}}"); + + int intVal = 2_000_000_000; + long longVal = 4_000_000_000L; + assertExpression( + intVal, + of(intVal + "").parseInteger()); + assertExpression( + longVal, + of(longVal + "").parseInteger()); + } + + // non-string + + @Test + public void millisecondsToDateTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ + assertExpression( + Instant.ofEpochMilli(1234), + of(1234L).millisecondsToDate(), + "{'$toDate': {'$numberLong': '1234'}}"); + // This does not accept plain integers: + assertThrows(MongoCommandException.class, () -> + assertExpression( + Instant.parse("2007-12-03T10:15:30.005Z"), + of(1234).millisecondsToDate(), + "{'$toDate': 1234}")); + } +}