From 72665fffe65be411aaabc42c2cc3eadabad10d1f Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 18 Nov 2022 11:04:24 -0700 Subject: [PATCH 01/11] Implement type checking/conversion expressions --- .../model/expressions/DateExpression.java | 3 +- .../client/model/expressions/Expression.java | 21 ++ .../client/model/expressions/Expressions.java | 6 +- .../model/expressions/MqlExpression.java | 121 +++++++-- .../model/expressions/NumberExpression.java | 2 + .../model/expressions/StringExpression.java | 15 +- .../AbstractExpressionsFunctionalTest.java | 7 +- .../ArithmeticExpressionsFunctionalTest.java | 18 ++ .../ArrayExpressionsFunctionalTest.java | 24 +- .../ComparisonExpressionsFunctionalTest.java | 15 +- .../DateExpressionsFunctionalTest.java | 9 +- .../TypeExpressionsFunctionalTest.java | 233 ++++++++++++++++++ 12 files changed, 438 insertions(+), 36 deletions(-) create mode 100644 driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java 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..06d6a829665 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,25 @@ 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); + + + /** + * server error if type cannot be converted to string (arrays, objects) + * TODO: should this be moved to non-array/obj types? + * + * @return + */ + 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..244f31881ce 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,72 @@ 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", "timestamp").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")); + } + + @Override + public IntegerExpression parseInteger() { + return new MqlExpression<>(ast("$toLong")); // TODO + } + /** @see ArrayExpression */ @Override @@ -191,10 +266,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 +277,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(); } @@ -252,10 +318,7 @@ public ArrayExpression union(final ArrayExpression set) { @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 +370,11 @@ public IntegerExpression abs() { return newMqlExpression(ast("$abs")); } + @Override + public DateExpression msToDate() { + return newMqlExpression(ast("$toDate")); + } + @Override public NumberExpression subtract(final NumberExpression n) { return new MqlExpression<>(ast("$subtract", n)); @@ -397,13 +465,27 @@ public StringExpression dateToString() { } @Override - public StringExpression dateToString(final StringExpression timezone, final StringExpression format) { + public StringExpression asString(final StringExpression timezone, final StringExpression format) { return newMqlExpression((cr) -> astDoc("$dateToString", new BsonDocument() .append("date", this.toBsonValue(cr)) .append("format", extractBsonValue(cr, format)) .append("timezone", extractBsonValue(cr, timezone)))); } + @Override + public DateExpression parseDate(final StringExpression format, final StringExpression timezone) { + 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() { + return newMqlExpression((cr) -> astDoc("$dateFromString", new BsonDocument() + .append("dateString", this.toBsonValue(cr)))); + } + /** @see StringExpression */ @Override @@ -440,4 +522,9 @@ public StringExpression substr(final IntegerExpression start, final IntegerExpre public StringExpression substrBytes(final IntegerExpression start, final IntegerExpression length) { return new MqlExpression<>(ast("$substrBytes", start, length)); } + + @Override + public Expression parseObjectId() { + return newMqlExpression(ast("$toObjectId")); + } } 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..e7edc1916c2 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 msToDate(); } 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..f6e5f2877be 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 @@ -16,6 +16,8 @@ package com.mongodb.client.model.expressions; +import com.mongodb.annotations.Beta; + import static com.mongodb.client.model.expressions.Expressions.of; /** @@ -35,13 +37,22 @@ 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, StringExpression timezone); + + @Beta(Beta.Reason.CLIENT) + Expression parseObjectId(); } 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..5de436b82d3 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 @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import static com.mongodb.ClusterFixture.serverVersionAtLeast; import static com.mongodb.client.model.Aggregates.addFields; @@ -54,7 +55,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 +75,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 == Optional.empty() && evaluated == null) { + // the "val" field was removed by "missing" + return; + } BsonValue expected1 = toBsonValue(expected); assertEquals(expected1, evaluated); } 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..3c4b58f4953 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 @@ -139,6 +139,17 @@ public void addTest() { of(2.0).add(of(2)), "{'$add': [2.0, 2]}"); + // overflow + 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)); @@ -225,6 +236,13 @@ 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()); } @Test 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..aa609c8dd12 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,12 +16,14 @@ package com.mongodb.client.model.expressions; +import com.mongodb.MongoCommandException; import org.bson.types.Decimal128; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.Arrays; import java.util.Collections; +import java.util.Optional; import java.util.LinkedList; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -33,6 +35,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,10 +180,14 @@ 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, @@ -188,6 +195,15 @@ public void elementAtTest() { assertExpression( true, ofRem().eq(array123.elementAt(-99))); + + assertExpression( + Optional.empty(), + array123.elementAt(-99)); + + // long values are considered entirely out of bounds; server error + assertThrows(MongoCommandException.class, () -> assertExpression( + Optional.empty(), + array123.elementAt(of(Long.MAX_VALUE)))); } @Test @@ -209,6 +225,12 @@ public void lastTest() { array123.last(), // MQL: "{'$last': [[1, 2, 3]]}"); + + assertExpression( + Optional.empty(), + ofIntegerArray().last(), + // MQL: + "{'$last': [[]]}"); } @Test 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..0b3a308bd78 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 @@ -26,7 +26,9 @@ 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 +43,7 @@ public void literalsTest() { instant, date, "{'$date': '2007-12-03T10:15:30.005Z'}"); + assertThrows(IllegalArgumentException.class, () -> of((Instant) null)); } @Test @@ -138,18 +141,18 @@ public void dateToStringTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ assertExpression( instant.toString(), - date.dateToString(), + date.asString(), "{'$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")), + 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.dateToString(of("+04:30"), of("%Y-%m-%dT%H:%M:%S.%L")), + 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'}}"); 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..880aacc4be4 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java @@ -0,0 +1,233 @@ +/* + * 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.types.Decimal128; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +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 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/ (28 |40) + // type is not implemented directly; instead, similar checks done via switch + + // the direct "isT" (comparable to instanceof) are exposed via switch + // here, we expose isTypeOr. These would be used on an Expression of + // an unknown type, or to provide default values in cases where a null + // has intruded into the alleged type. + + @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/ (99 |87) + 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( + Instant.ofEpochMilli(1196676930005L), + of(date).isDateOr(of(date.plusMillis(10))), + "{'$cond': [{'$in': [{'$type': {'$date': '2007-12-03T10:15:30.005Z'}}, " + + "['date', 'timestamp']]}, {'$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/ (36 |47) + 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/ (38 |40) + // Convert is not implemented: too dynamic, conversions should be explicit. + + /* + Useful conversions: + - anything-string: toString to a parsable type + - string-parse-anything: every type should allow parsing to a string + - presently excludes objects (docs) and arrays + - includes formatted date strings + - milliseconds since epoch to date + + Convert also defines many conversions that do not seem useful: + - boolean-number conversions: t/f to-from 1/0 + - boolean-other (oid/str/date) - always true, broken for strings; pointless + - number-number - "underlying" json type is changed, possible exceptions? + */ + + @Test + public void asStringTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/ + // asString, since toString conflicts + assertExpression("false", of(false).asString()); + + 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()); + + // TODO: +// assertExpression("[]", ofIntegerArray(1, 2).asString()); +// assertExpression("[1, 2]", ofIntegerArray(1, 2).asString()); +// 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"); + assertExpression( + "2007-12-03T10:15:30.005Z", + of(instant).asString()); + + // with parameters + assertExpression( + "2007-12-03T05:15:30.005Z", + of(instant).asString(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("America/New_York")), + "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', " + + "'timezone': 'America/New_York'}}"); + assertExpression( + "2007-12-03T14:45:30.005Z", + of(instant).asString(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("+04:30")), + "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', " + + "'timezone': '+04:30'}}"); + } + + // parse string + + @Test + public void parseDateTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/ (45 |69) + String stringDate = "2007-12-03T10:15:30.005Z"; + final Instant instant = Instant.parse(stringDate); + assertExpression( + instant, + of(stringDate).parseDate(), + "{'$dateFromString': {'dateString': '2007-12-03T10:15:30.005Z'}}"); + + // with parameters + assertExpression( + instant, + of("2007-12-03T05:15:30.005Z").parseDate(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("America/New_York")), + "{'$dateFromString': {'dateString': '2007-12-03T05:15:30.005Z', " + + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', 'timezone': 'America/New_York'}}"); + assertExpression( + instant, + of("2007-12-03T14:45:30.005Z").parseDate(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("+04:30")), + "{'$dateFromString': {'dateString': '2007-12-03T14:45:30.005Z', " + + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', 'timezone': '+04:30'}}"); + } + + @Test + public void parseIntegerTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ (46 |15) + assertExpression(1234L, of("1234").parseInteger()); + // TODO: note that this parses to long. Unclear how to dynamically choose int/long + } + + @Test + public void parseObjectIdTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/ (39 |28) + // TODO objectId has no type of its own, but this might be fine + assertExpression( + new ObjectId("5ab9cbfa31c2ab715d42129e"), + of("5ab9cbfa31c2ab715d42129e").parseObjectId()); + } + + // non-string + + @Test + public void msToDateTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ (36 |53) + assertExpression(Instant.ofEpochMilli(1234), of(1234L).msToDate()); + // could be: millisecondsToDate / epochMsToDate + // TODO does not accept plain integers; could convert to dec128? + assertThrows(MongoCommandException.class, () -> + assertExpression(Instant.parse("2007-12-03T10:15:30.005Z"), of(1234).msToDate())); + } + +} From 7c0d502d290fd6cd56e767129802382318cf6a05 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 18 Nov 2022 12:50:09 -0700 Subject: [PATCH 02/11] Add missing mql --- .../TypeExpressionsFunctionalTest.java | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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 index 880aacc4be4..755ef8e2753 100644 --- 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 @@ -136,7 +136,7 @@ public void isDocumentOrTest() { public void asStringTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/ // asString, since toString conflicts - assertExpression("false", of(false).asString()); + assertExpression("false", of(false).asString(), "{'$toString': [false]}"); assertExpression("1", of(1).asString()); assertExpression("1", of(1L).asString()); @@ -160,7 +160,8 @@ public void dateAsStringTest() { final Instant instant = Instant.parse("2007-12-03T10:15:30.005Z"); assertExpression( "2007-12-03T10:15:30.005Z", - of(instant).asString()); + of(instant).asString(), + "{'$toString': [{'$date': '2007-12-03T10:15:30.005Z'}]}"); // with parameters assertExpression( @@ -205,7 +206,7 @@ public void parseDateTest() { @Test public void parseIntegerTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ (46 |15) - assertExpression(1234L, of("1234").parseInteger()); + assertExpression(1234L, of("1234").parseInteger(), "{'$toLong': '1234'}"); // TODO: note that this parses to long. Unclear how to dynamically choose int/long } @@ -215,7 +216,8 @@ public void parseObjectIdTest() { // TODO objectId has no type of its own, but this might be fine assertExpression( new ObjectId("5ab9cbfa31c2ab715d42129e"), - of("5ab9cbfa31c2ab715d42129e").parseObjectId()); + of("5ab9cbfa31c2ab715d42129e").parseObjectId(), + "{'$toObjectId': '5ab9cbfa31c2ab715d42129e'}"); } // non-string @@ -223,11 +225,16 @@ public void parseObjectIdTest() { @Test public void msToDateTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ (36 |53) - assertExpression(Instant.ofEpochMilli(1234), of(1234L).msToDate()); + assertExpression( + Instant.ofEpochMilli(1234), + of(1234L).msToDate(), + "{'$toDate': {'$numberLong': '1234'}}"); // could be: millisecondsToDate / epochMsToDate // TODO does not accept plain integers; could convert to dec128? assertThrows(MongoCommandException.class, () -> - assertExpression(Instant.parse("2007-12-03T10:15:30.005Z"), of(1234).msToDate())); + assertExpression( + Instant.parse("2007-12-03T10:15:30.005Z"), + of(1234).msToDate(), + "{'$toDate': 1234}")); } - } From 5df68ed6e9cfce6910231a2f2d1edc3750ab41db Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 21 Nov 2022 16:41:01 -0700 Subject: [PATCH 03/11] Post-rebase fixes --- .../model/expressions/MqlExpression.java | 6 --- .../DateExpressionsFunctionalTest.java | 22 ---------- .../TypeExpressionsFunctionalTest.java | 43 +++++++++++-------- 3 files changed, 25 insertions(+), 46 deletions(-) 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 244f31881ce..a52400358aa 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 @@ -458,12 +458,6 @@ public IntegerExpression millisecond(final StringExpression timezone) { return usingTimezone("$millisecond", timezone); } - @Override - public StringExpression dateToString() { - return newMqlExpression((cr) -> astDoc("$dateToString", new BsonDocument() - .append("date", this.toBsonValue(cr)))); - } - @Override public StringExpression asString(final StringExpression timezone, final StringExpression format) { return newMqlExpression((cr) -> astDoc("$dateToString", new BsonDocument() 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 0b3a308bd78..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,7 +25,6 @@ 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") @@ -136,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.asString(), - "{'$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.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'}}"); - } } 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 index 755ef8e2753..1fc5fc802bd 100644 --- 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 @@ -23,11 +23,15 @@ 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 { @@ -166,41 +170,44 @@ public void dateAsStringTest() { // with parameters assertExpression( "2007-12-03T05:15:30.005Z", - of(instant).asString(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("America/New_York")), + of(instant).asString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%LZ")), "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', " + "'timezone': 'America/New_York'}}"); assertExpression( "2007-12-03T14:45:30.005Z", - of(instant).asString(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("+04:30")), + of(instant).asString(of("+04:30"), of("%Y-%m-%dT%H:%M:%S.%LZ")), "{'$dateToString': {'date': {'$date': '2007-12-03T10:15:30.005Z'}, " + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', " + "'timezone': '+04:30'}}"); } + // parse string @Test - public void parseDateTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/ (45 |69) - String stringDate = "2007-12-03T10:15:30.005Z"; - final Instant instant = Instant.parse(stringDate); + public void dateToStringTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ + 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( - instant, - of(stringDate).parseDate(), - "{'$dateFromString': {'dateString': '2007-12-03T10:15:30.005Z'}}"); - + instant.toString(), + date.asString(), + "{'$toString': [{'$date': '2007-12-03T10:15:30.005Z'}]}"); // with parameters assertExpression( - instant, - of("2007-12-03T05:15:30.005Z").parseDate(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("America/New_York")), - "{'$dateFromString': {'dateString': '2007-12-03T05:15:30.005Z', " - + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', 'timezone': 'America/New_York'}}"); + 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( - instant, - of("2007-12-03T14:45:30.005Z").parseDate(of("%Y-%m-%dT%H:%M:%S.%LZ"), of("+04:30")), - "{'$dateFromString': {'dateString': '2007-12-03T14:45:30.005Z', " - + "'format': '%Y-%m-%dT%H:%M:%S.%LZ', 'timezone': '+04:30'}}"); + 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'}}"); } @Test From 136f6cd8e855b4e75663426a43732ee4e21c1b68 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 25 Nov 2022 12:13:15 -0700 Subject: [PATCH 04/11] Fixes --- .../model/expressions/ArrayExpression.java | 4 +- .../model/expressions/MqlExpression.java | 22 ++-- .../model/expressions/NumberExpression.java | 2 +- .../model/expressions/StringExpression.java | 5 +- .../AbstractExpressionsFunctionalTest.java | 10 +- .../ArrayExpressionsFunctionalTest.java | 25 +++-- .../TypeExpressionsFunctionalTest.java | 106 +++++++++--------- 7 files changed, 93 insertions(+), 81 deletions(-) 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/MqlExpression.java b/driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java index a52400358aa..24a6302610a 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 @@ -198,7 +198,7 @@ public StringExpression isStringOr(final StringExpression or) { } public BooleanExpression isDate() { - return ofStringArray("date", "timestamp").contains(new MqlExpression<>(ast("$type"))); + return ofStringArray("date").contains(new MqlExpression<>(ast("$type"))); } @Override @@ -299,7 +299,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(); } @@ -311,7 +311,7 @@ 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(); } @@ -371,7 +371,7 @@ public IntegerExpression abs() { } @Override - public DateExpression msToDate() { + public DateExpression millisecondsToDate() { return newMqlExpression(ast("$toDate")); } @@ -467,13 +467,20 @@ public StringExpression asString(final StringExpression timezone, final StringEx } @Override - public DateExpression parseDate(final StringExpression format, final StringExpression timezone) { + 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() @@ -516,9 +523,4 @@ public StringExpression substr(final IntegerExpression start, final IntegerExpre public StringExpression substrBytes(final IntegerExpression start, final IntegerExpression length) { return new MqlExpression<>(ast("$substrBytes", start, length)); } - - @Override - public Expression parseObjectId() { - return newMqlExpression(ast("$toObjectId")); - } } 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 e7edc1916c2..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 @@ -55,5 +55,5 @@ default NumberExpression subtract(final Number subtract) { NumberExpression abs(); - DateExpression msToDate(); + 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 f6e5f2877be..f69648b21f0 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 @@ -51,8 +51,7 @@ default StringExpression substrBytes(final int start, final int length) { DateExpression parseDate(); - DateExpression parseDate(StringExpression format, StringExpression timezone); + DateExpression parseDate(StringExpression format); - @Beta(Beta.Reason.CLIENT) - Expression parseObjectId(); + 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 5de436b82d3..3236d32f3bf 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 @@ -45,6 +45,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(); @@ -75,8 +80,8 @@ 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 == Optional.empty() && evaluated == null) { - // the "val" field was removed by "missing" + if (expected == MISSING && evaluated == null) { + // ig the "val" field was removed by "missing", then evaluated is null return; } BsonValue expected1 = toBsonValue(expected); @@ -90,6 +95,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/ArrayExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/ArrayExpressionsFunctionalTest.java index aa609c8dd12..031ce9a122c 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 @@ -190,19 +190,16 @@ public void elementAtTest() { array123.elementAt(of(1L))); assertExpression( - true, - ofRem().eq(array123.elementAt(99))); - assertExpression( - true, - ofRem().eq(array123.elementAt(-99))); + MISSING, + array123.elementAt(99)); assertExpression( - Optional.empty(), + MISSING, array123.elementAt(-99)); // long values are considered entirely out of bounds; server error assertThrows(MongoCommandException.class, () -> assertExpression( - Optional.empty(), + MISSING, array123.elementAt(of(Long.MAX_VALUE)))); } @@ -227,7 +224,7 @@ public void lastTest() { "{'$last': [[1, 2, 3]]}"); assertExpression( - Optional.empty(), + MISSING, ofIntegerArray().last(), // MQL: "{'$last': [[]]}"); @@ -250,9 +247,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 @@ -286,6 +287,12 @@ public void setUnionTest() { 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).setUnion(ofIntegerArray(1, 2, 3))); // convenience assertExpression( Arrays.asList(1, 2, 3), 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 index 1fc5fc802bd..d9bed37c7f1 100644 --- 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 @@ -18,8 +18,8 @@ import com.mongodb.MongoCommandException; import org.bson.BsonDocument; +import org.bson.Document; import org.bson.types.Decimal128; -import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -40,8 +40,9 @@ class TypeExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/ (28 |40) // type is not implemented directly; instead, similar checks done via switch - // the direct "isT" (comparable to instanceof) are exposed via switch - // here, we expose isTypeOr. These would be used on an Expression of + // The direct "isT" (comparable to instanceof) methods, which one might + // expect to see on Expression, are exposed via switch. + // Here, we only expose isTypeOr. These would be used on an Expression of // an unknown type, or to provide default values in cases where a null // has intruded into the alleged type. @@ -84,11 +85,10 @@ public void isStringOrTest() { public void isDateOrTest() { Instant date = Instant.parse("2007-12-03T10:15:30.005Z"); assertExpression( - Instant.ofEpochMilli(1196676930005L), + date, of(date).isDateOr(of(date.plusMillis(10))), - "{'$cond': [{'$in': [{'$type': {'$date': '2007-12-03T10:15:30.005Z'}}, " - + "['date', 'timestamp']]}, {'$date': '2007-12-03T10:15:30.005Z'}, " - + "{'$date': '2007-12-03T10:15:30.015Z'}]}"); + "{'$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))); @@ -123,8 +123,11 @@ public void isDocumentOrTest() { // Convert is not implemented: too dynamic, conversions should be explicit. /* + One might expect to see all conversions in $convert represented in this + API, but we expose only the useful ones. + Useful conversions: - - anything-string: toString to a parsable type + - anything-string: toString to a parsable type (excludes doc, array) - string-parse-anything: every type should allow parsing to a string - presently excludes objects (docs) and arrays - includes formatted date strings @@ -152,62 +155,68 @@ public void asStringTest() { // this is equivalent to $dateToString assertExpression("1970-01-01T00:00:00.123Z", of(Instant.ofEpochMilli(123)).asString()); - // TODO: -// assertExpression("[]", ofIntegerArray(1, 2).asString()); -// assertExpression("[1, 2]", ofIntegerArray(1, 2).asString()); -// assertExpression("{a: 1}", of(Document.parse("{a: 1}")).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( - "2007-12-03T05:15:30.005Z", - of(instant).asString(of("America/New_York"), of("%Y-%m-%dT%H:%M:%S.%LZ")), + 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.%LZ', " + + "'format': '%Y-%m-%dT%H:%M:%S.%L', " + "'timezone': 'America/New_York'}}"); assertExpression( - "2007-12-03T14:45:30.005Z", - of(instant).asString(of("+04:30"), of("%Y-%m-%dT%H:%M:%S.%LZ")), + 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.%LZ', " + + "'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 dateToStringTest() { + public void parseDateTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ - Instant instant = Instant.parse("2007-12-03T10:15:30.005Z"); - DateExpression date = of(instant); - ZonedDateTime utcDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of(ZoneOffset.UTC.getId())); + String dateString = "2007-12-03T10:15:30.005Z"; assertExpression( - instant.toString(), - date.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'}}"); + 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( - 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'}}"); + 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 @@ -217,31 +226,20 @@ public void parseIntegerTest() { // TODO: note that this parses to long. Unclear how to dynamically choose int/long } - @Test - public void parseObjectIdTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/ (39 |28) - // TODO objectId has no type of its own, but this might be fine - assertExpression( - new ObjectId("5ab9cbfa31c2ab715d42129e"), - of("5ab9cbfa31c2ab715d42129e").parseObjectId(), - "{'$toObjectId': '5ab9cbfa31c2ab715d42129e'}"); - } - // non-string @Test - public void msToDateTest() { + public void millisecondsToDateTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ (36 |53) assertExpression( Instant.ofEpochMilli(1234), - of(1234L).msToDate(), + of(1234L).millisecondsToDate(), "{'$toDate': {'$numberLong': '1234'}}"); - // could be: millisecondsToDate / epochMsToDate - // TODO does not accept plain integers; could convert to dec128? + // This does not accept plain integers: assertThrows(MongoCommandException.class, () -> assertExpression( Instant.parse("2007-12-03T10:15:30.005Z"), - of(1234).msToDate(), + of(1234).millisecondsToDate(), "{'$toDate': 1234}")); } } From 17686ad664d959af62f0d3cc3acb3a1202164996 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 28 Nov 2022 14:20:33 -0700 Subject: [PATCH 05/11] Remove TODOs --- .../main/com/mongodb/client/model/expressions/Expression.java | 1 - .../com/mongodb/client/model/expressions/MqlExpression.java | 2 +- .../mongodb/client/model/expressions/StringExpression.java | 4 ++++ .../model/expressions/TypeExpressionsFunctionalTest.java | 1 - 4 files changed, 5 insertions(+), 3 deletions(-) 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 06d6a829665..1fcd9177062 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 @@ -113,7 +113,6 @@ public interface Expression { /** * server error if type cannot be converted to string (arrays, objects) - * TODO: should this be moved to non-array/obj types? * * @return */ 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 24a6302610a..0af999d7c72 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 @@ -233,7 +233,7 @@ public StringExpression asString() { @Override public IntegerExpression parseInteger() { - return new MqlExpression<>(ast("$toLong")); // TODO + return new MqlExpression<>(ast("$toLong")); } /** @see ArrayExpression */ 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 f69648b21f0..3e4a580e5f4 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 @@ -47,6 +47,10 @@ default StringExpression substrBytes(final int start, final int length) { return this.substrBytes(of(start), of(length)); } + /** + * parses to an underlying int64 + * @return + */ IntegerExpression parseInteger(); DateExpression parseDate(); 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 index d9bed37c7f1..fcab1c477f8 100644 --- 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 @@ -223,7 +223,6 @@ public void parseDateTest() { public void parseIntegerTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ (46 |15) assertExpression(1234L, of("1234").parseInteger(), "{'$toLong': '1234'}"); - // TODO: note that this parses to long. Unclear how to dynamically choose int/long } // non-string From 0f15a6771b164fa8999dfeb326563c7ca595e9bb Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 29 Nov 2022 09:44:23 -0700 Subject: [PATCH 06/11] Fixes --- .../com/mongodb/client/model/expressions/StringExpression.java | 2 -- .../model/expressions/AbstractExpressionsFunctionalTest.java | 1 - .../model/expressions/ArrayExpressionsFunctionalTest.java | 3 +-- .../model/expressions/TypeExpressionsFunctionalTest.java | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) 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 3e4a580e5f4..5fbafb9a2b5 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 @@ -16,8 +16,6 @@ package com.mongodb.client.model.expressions; -import com.mongodb.annotations.Beta; - import static com.mongodb.client.model.expressions.Expressions.of; /** 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 3236d32f3bf..e4d28c0f134 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 @@ -36,7 +36,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Optional; import static com.mongodb.ClusterFixture.serverVersionAtLeast; import static com.mongodb.client.model.Aggregates.addFields; 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 031ce9a122c..d1d82d986a7 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 @@ -23,7 +23,6 @@ import java.time.Instant; import java.util.Arrays; import java.util.Collections; -import java.util.Optional; import java.util.LinkedList; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -292,7 +291,7 @@ public void setUnionTest() { 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).setUnion(ofIntegerArray(1, 2, 3))); + 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/TypeExpressionsFunctionalTest.java b/driver-core/src/test/functional/com/mongodb/client/model/expressions/TypeExpressionsFunctionalTest.java index fcab1c477f8..b0fc5e854de 100644 --- 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 @@ -207,7 +207,7 @@ public void parseDateTest() { // 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") + 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: From 7050dfb9e5256559f4f69fe5aa54bdd15ed12249 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Wed, 30 Nov 2022 14:27:58 -0700 Subject: [PATCH 07/11] Fixes --- .../client/model/expressions/Expression.java | 7 ---- .../AbstractExpressionsFunctionalTest.java | 2 +- .../ArithmeticExpressionsFunctionalTest.java | 4 +-- .../ArrayExpressionsFunctionalTest.java | 2 +- .../StringExpressionsFunctionalTest.java | 10 +++--- .../TypeExpressionsFunctionalTest.java | 35 ++++--------------- 6 files changed, 15 insertions(+), 45 deletions(-) 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 1fcd9177062..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 @@ -109,12 +109,5 @@ public interface Expression { DateExpression isDateOr(DateExpression or); ArrayExpression isArrayOr(ArrayExpression or); T isDocumentOr(T or); - - - /** - * server error if type cannot be converted to string (arrays, objects) - * - * @return - */ StringExpression asString(); } 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 e4d28c0f134..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 @@ -80,7 +80,7 @@ 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) { - // ig the "val" field was removed by "missing", then evaluated is null + // if the "val" field was removed by "missing", then evaluated is null return; } BsonValue expected1 = toBsonValue(expected); 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 3c4b58f4953..72885c01321 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 @@ -194,7 +194,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), @@ -247,7 +247,7 @@ public void roundTest() { @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 d1d82d986a7..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 @@ -280,7 +280,7 @@ 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), 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 index b0fc5e854de..0a3c93a458d 100644 --- 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 @@ -37,15 +37,9 @@ 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/ (28 |40) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/ // type is not implemented directly; instead, similar checks done via switch - // The direct "isT" (comparable to instanceof) methods, which one might - // expect to see on Expression, are exposed via switch. - // Here, we only expose isTypeOr. These would be used on an Expression of - // an unknown type, or to provide default values in cases where a null - // has intruded into the alleged type. - @Test public void isBooleanOrTest() { assertExpression( @@ -59,7 +53,7 @@ public void isBooleanOrTest() { @Test public void isNumberOrTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/ (99 |87) + // 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))); @@ -96,7 +90,7 @@ public void isDateOrTest() { @Test public void isArrayOrTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/ (36 |47) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/ assertExpression( Arrays.asList(1, 2), ofIntegerArray(1, 2).isArrayOr(ofIntegerArray(99)), @@ -119,26 +113,9 @@ public void isDocumentOrTest() { } // conversions - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/ (38 |40) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/ // Convert is not implemented: too dynamic, conversions should be explicit. - /* - One might expect to see all conversions in $convert represented in this - API, but we expose only the useful ones. - - Useful conversions: - - anything-string: toString to a parsable type (excludes doc, array) - - string-parse-anything: every type should allow parsing to a string - - presently excludes objects (docs) and arrays - - includes formatted date strings - - milliseconds since epoch to date - - Convert also defines many conversions that do not seem useful: - - boolean-number conversions: t/f to-from 1/0 - - boolean-other (oid/str/date) - always true, broken for strings; pointless - - number-number - "underlying" json type is changed, possible exceptions? - */ - @Test public void asStringTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/ @@ -221,7 +198,7 @@ public void parseDateTest() { @Test public void parseIntegerTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ (46 |15) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ assertExpression(1234L, of("1234").parseInteger(), "{'$toLong': '1234'}"); } @@ -229,7 +206,7 @@ public void parseIntegerTest() { @Test public void millisecondsToDateTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ (36 |53) + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ assertExpression( Instant.ofEpochMilli(1234), of(1234L).millisecondsToDate(), From f6582cfaae91516c1bcddbf4c11565df1f813d63 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Thu, 1 Dec 2022 15:19:08 -0700 Subject: [PATCH 08/11] Add parseNumber --- .../client/model/expressions/MqlExpression.java | 5 +++++ .../client/model/expressions/StringExpression.java | 2 ++ .../expressions/TypeExpressionsFunctionalTest.java | 13 +++++++++++++ 3 files changed, 20 insertions(+) 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 0af999d7c72..e0b8b192e48 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 @@ -236,6 +236,11 @@ public IntegerExpression parseInteger() { return new MqlExpression<>(ast("$toLong")); } + @Override + public NumberExpression parseNumber() { + return new MqlExpression<>(ast("$toDecimal")); + } + /** @see ArrayExpression */ @Override 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 5fbafb9a2b5..84348f18117 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 @@ -51,6 +51,8 @@ default StringExpression substrBytes(final int start, final int length) { */ IntegerExpression parseInteger(); + NumberExpression parseNumber(); + DateExpression parseDate(); DateExpression parseDate(StringExpression format); 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 index 0a3c93a458d..deb7220fb4e 100644 --- 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 @@ -202,6 +202,19 @@ public void parseIntegerTest() { assertExpression(1234L, of("1234").parseInteger(), "{'$toLong': '1234'}"); } + @Test + public void parseNumberTest() { + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ + assertExpression( + Decimal128.parse("1234.200"), + of("1234.200").parseNumber(), + "{'$toDecimal': '1234.200'}"); + + assertExpression( + true, + of("1234").parseInteger().eq(of("1234").parseNumber())); + } + // non-string @Test From b9937548ae5dc48d05509316127e86c8fa6738bc Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Fri, 9 Dec 2022 10:55:06 -0700 Subject: [PATCH 09/11] Better round-tripping of BSON --- .../model/expressions/MqlExpression.java | 13 +++++- .../model/expressions/StringExpression.java | 4 -- .../ArithmeticExpressionsFunctionalTest.java | 26 +++++++++-- .../TypeExpressionsFunctionalTest.java | 45 ++++++++++++++++--- 4 files changed, 73 insertions(+), 15 deletions(-) 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 e0b8b192e48..81231d13a7d 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 @@ -231,14 +231,23 @@ public StringExpression asString() { return new MqlExpression<>(astWrapped("$toString")); } + private Function convertInternal(String to, 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() { - return new MqlExpression<>(ast("$toLong")); + Expression asLong = new MqlExpression<>(ast("$toLong")); + return new MqlExpression<>(convertInternal("int", asLong)); } @Override public NumberExpression parseNumber() { - return new MqlExpression<>(ast("$toDecimal")); + Expression asDecimal = new MqlExpression<>(ast("$toDecimal")); + return new MqlExpression<>(convertInternal("double", asDecimal)); } /** @see ArrayExpression */ 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 84348f18117..4b905484aaa 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 @@ -45,10 +45,6 @@ default StringExpression substrBytes(final int start, final int length) { return this.substrBytes(of(start), of(length)); } - /** - * parses to an underlying int64 - * @return - */ IntegerExpression parseInteger(); NumberExpression parseNumber(); 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 72885c01321..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,7 +156,7 @@ public void addTest() { of(2.0).add(of(2)), "{'$add': [2.0, 2]}"); - // overflow + // overflows into a supported underlying type assertExpression( Integer.MAX_VALUE + 2L, of(Integer.MAX_VALUE).add(of(2))); @@ -243,6 +260,9 @@ public void roundTest() { assertExpression( 5.0, of(5.0).round()); + assertExpression( + Decimal128.parse("1234"), + of(Decimal128.parse("1234.2")).round()); } @Test 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 index deb7220fb4e..53b30d81ed3 100644 --- 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 @@ -22,6 +22,7 @@ import org.bson.types.Decimal128; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; @@ -199,20 +200,52 @@ public void parseDateTest() { @Test public void parseIntegerTest() { // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ - assertExpression(1234L, of("1234").parseInteger(), "{'$toLong': '1234'}"); + // 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()); } @Test public void parseNumberTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/ + // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/ assertExpression( - Decimal128.parse("1234.200"), + 1234.2, of("1234.200").parseNumber(), - "{'$toDecimal': '1234.200'}"); - + "{'$convert': {'input': '1234.200', 'onError': {'$toDecimal': '1234.200'}, 'to': 'double'}}"); + assertExpression( + 1234.0, + of("1234").parseNumber()); assertExpression( true, - of("1234").parseInteger().eq(of("1234").parseNumber())); + of("1234").parseInteger().eq( + of("1234").parseNumber())); + + double doubleVal = 2.5; + Decimal128 decimalVal = Decimal128.parse("2.5999999999999999"); + Decimal128 decimalVal2 = new Decimal128(BigDecimal.valueOf(Double.MAX_VALUE).multiply(BigDecimal.TEN)); + assertExpression( + doubleVal, + of(doubleVal + "").parseNumber()); + // some small decimals are readable as doubles, but rounded: + assertExpression( + 2.6, + of(decimalVal + "").parseNumber()); + // decimals exceeding max double are read as decimals: + assertExpression( + decimalVal2, + of(decimalVal2 + "").parseNumber()); } // non-string From fb9ad85f865128abb5a5e5e558ef054dbc7e1e28 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 12 Dec 2022 10:37:08 -0700 Subject: [PATCH 10/11] Remove parseNumber --- .../model/expressions/MqlExpression.java | 6 ---- .../model/expressions/StringExpression.java | 2 -- .../TypeExpressionsFunctionalTest.java | 32 ------------------- 3 files changed, 40 deletions(-) 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 81231d13a7d..a39b108b40e 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 @@ -244,12 +244,6 @@ public IntegerExpression parseInteger() { return new MqlExpression<>(convertInternal("int", asLong)); } - @Override - public NumberExpression parseNumber() { - Expression asDecimal = new MqlExpression<>(ast("$toDecimal")); - return new MqlExpression<>(convertInternal("double", asDecimal)); - } - /** @see ArrayExpression */ @Override 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 4b905484aaa..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 @@ -47,8 +47,6 @@ default StringExpression substrBytes(final int start, final int length) { IntegerExpression parseInteger(); - NumberExpression parseNumber(); - DateExpression parseDate(); DateExpression parseDate(StringExpression format); 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 index 53b30d81ed3..47b67025555 100644 --- 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 @@ -216,38 +216,6 @@ public void parseIntegerTest() { of(longVal + "").parseInteger()); } - @Test - public void parseNumberTest() { - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/ - // https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/ - assertExpression( - 1234.2, - of("1234.200").parseNumber(), - "{'$convert': {'input': '1234.200', 'onError': {'$toDecimal': '1234.200'}, 'to': 'double'}}"); - assertExpression( - 1234.0, - of("1234").parseNumber()); - assertExpression( - true, - of("1234").parseInteger().eq( - of("1234").parseNumber())); - - double doubleVal = 2.5; - Decimal128 decimalVal = Decimal128.parse("2.5999999999999999"); - Decimal128 decimalVal2 = new Decimal128(BigDecimal.valueOf(Double.MAX_VALUE).multiply(BigDecimal.TEN)); - assertExpression( - doubleVal, - of(doubleVal + "").parseNumber()); - // some small decimals are readable as doubles, but rounded: - assertExpression( - 2.6, - of(decimalVal + "").parseNumber()); - // decimals exceeding max double are read as decimals: - assertExpression( - decimalVal2, - of(decimalVal2 + "").parseNumber()); - } - // non-string @Test From 53af31c37606afc53e1f1daf7f0d1eb53d849649 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Mon, 12 Dec 2022 10:47:13 -0700 Subject: [PATCH 11/11] Fixes --- .../com/mongodb/client/model/expressions/MqlExpression.java | 2 +- .../client/model/expressions/TypeExpressionsFunctionalTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 a39b108b40e..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 @@ -231,7 +231,7 @@ public StringExpression asString() { return new MqlExpression<>(astWrapped("$toString")); } - private Function convertInternal(String to, Expression orElse) { + 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)) 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 index 47b67025555..1f9daf28a3a 100644 --- 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 @@ -22,7 +22,6 @@ import org.bson.types.Decimal128; import org.junit.jupiter.api.Test; -import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset;