From c633293f1dab303425969269bc93b9cc52bef96c Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Mon, 5 Jun 2023 17:54:40 -0700 Subject: [PATCH 1/8] Improve Time-Series Bucketing Scalability Introduce customizable bucketMaxSpanSeconds and bucketRoundingSeconds options for Time-Series collections, providing users with more control over bucketing behaviors. These options allow for setting the maximum time difference between timestamps within a bucket and adjusting the rounding precision. Enhancing flexibility in managing Time-Series data. JAVA-4888 --- .../client/model/TimeSeriesOptions.java | 95 +++++++++++++++++++ .../operation/CreateCollectionOperation.java | 10 ++ .../timeseries-collection.json | 65 +++++++++++++ .../client/unified/UnifiedCrudHelper.java | 6 ++ 4 files changed, 176 insertions(+) diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index 82e6356ab1e..d144d18ff29 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -18,6 +18,9 @@ import com.mongodb.lang.Nullable; +import java.util.concurrent.TimeUnit; + +import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; /** @@ -31,6 +34,8 @@ public final class TimeSeriesOptions { private final String timeField; private String metaField; private TimeSeriesGranularity granularity; + private Long bucketMaxSpanSeconds; + private Long bucketRoundingSeconds; /** * Construct a new instance. @@ -104,12 +109,102 @@ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granu return this; } + /** + * Returns the maximum time span between measurements in a bucket. + * + * @param timeUnit the time unit. + * @return time span between measurements. + * @mongodb.server.release 6.3 + * @mongodb.driver.manual core/timeseries-collections/ Time-series collections + * @see #bucketMaxSpan(Long, TimeUnit) + * @since 4.10 + */ + @Nullable + public Long getBucketMaxSpan(final TimeUnit timeUnit) { + if (bucketMaxSpanSeconds == null) { + return null; + } + return timeUnit.convert(bucketMaxSpanSeconds, TimeUnit.SECONDS); + } + + /** + * Sets the maximum time span between measurements in a bucket. + *

+ * The value of {@code bucketMaxSpanSeconds} must be the same as {@code bucketRoundingSeconds}. + * If you set the bucketMaxSpanSeconds, parameter, you can't set the granularity parameter + *

+ * + * @param bucketMaxSpan - time span between measurements. After conversion to seconds using + * {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, the value must be >= 1. + * @param timeUnit - the time unit. + * @return this + * @mongodb.server.release 6.3 + * @mongodb.driver.manual core/timeseries-collections/ Time-series collections + * @see #getBucketMaxSpan(TimeUnit) + * @since 4.10 + */ + public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final TimeUnit timeUnit) { + if (bucketMaxSpan == null) { + this.bucketMaxSpanSeconds = null; + } else { + this.bucketMaxSpanSeconds = TimeUnit.SECONDS.convert(bucketMaxSpan, timeUnit); + isTrueArgument("bucketMaxSpan, after conversion to seconds, must be >= 1", bucketMaxSpanSeconds > 0); + } + return this; + } + + /** + * Returns the time interval that determines the starting timestamp for a new bucket. + * + * @param timeUnit the time unit. + * @return the time interval. + * @mongodb.server.release 6.3 + * @mongodb.driver.manual core/timeseries-collections/ Time-series collections + * @see #bucketRounding(Long, TimeUnit) + * @since 4.10 + */ + @Nullable + public Long getBucketRounding(final TimeUnit timeUnit) { + if (bucketRoundingSeconds == null) { + return null; + } + return timeUnit.convert(bucketRoundingSeconds, TimeUnit.SECONDS); + } + + /** + * Specifies the time interval that determines the starting timestamp for a new bucket. + *

+ * The value of {@code bucketRounding} must be the same as {@code bucketMaxSpan}. If you set the {@code bucketRounding}, parameter, + * you can't set the granularity parameter. + *

+ * + * @param bucketRounding - time interval. After conversion to seconds using + * {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, the value must be >= 1. + * @param timeUnit - the time unit. + * @return this + * @mongodb.server.release 6.3 + * @mongodb.driver.manual core/timeseries-collections/ Time-series collections + * @see #getBucketRounding(TimeUnit) + * @since 4.10 + */ + public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, final TimeUnit timeUnit) { + if (bucketRounding == null) { + this.bucketRoundingSeconds = null; + } else { + this.bucketRoundingSeconds = TimeUnit.SECONDS.convert(bucketRounding, timeUnit); + isTrueArgument("bucketRounding, after conversion to seconds, must be >= 1", bucketMaxSpanSeconds > 0); + } + return this; + } + @Override public String toString() { return "TimeSeriesOptions{" + "timeField='" + timeField + '\'' + ", metaField='" + metaField + '\'' + ", granularity=" + granularity + + ", bucketMaxSpanSeconds=" + bucketMaxSpanSeconds + + ", bucketRoundingSeconds=" + bucketRoundingSeconds + '}'; } } diff --git a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java index ef2e1f706e9..feb296d6558 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CreateCollectionOperation.java @@ -34,11 +34,13 @@ import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; +import org.bson.BsonInt64; import org.bson.BsonString; import java.util.ArrayDeque; import java.util.Deque; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.notNull; @@ -347,6 +349,14 @@ private BsonDocument getCreateCollectionCommand() { if (granularity != null) { timeSeriesDocument.put("granularity", new BsonString(getGranularityAsString(granularity))); } + Long bucketMaxSpan = timeSeriesOptions.getBucketMaxSpan(TimeUnit.SECONDS); + if (bucketMaxSpan != null){ + timeSeriesDocument.put("bucketMaxSpanSeconds", new BsonInt64(bucketMaxSpan)); + } + Long bucketRounding = timeSeriesOptions.getBucketRounding(TimeUnit.SECONDS); + if (bucketRounding != null){ + timeSeriesDocument.put("bucketRoundingSeconds", new BsonInt64(bucketRounding)); + } document.put("timeseries", timeSeriesDocument); } if (changeStreamPreAndPostImagesOptions != null) { diff --git a/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json b/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json index b5638fd36e9..8525056fd1a 100644 --- a/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json +++ b/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json @@ -250,6 +250,71 @@ ] } ] + }, + { + "description": "createCollection with bucketing options", + "runOnRequirements": [ + { + "minServerVersion": "7.0" + } + ], + "operations": [ + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "collection": "test" + } + }, + { + "name": "createCollection", + "object": "database0", + "arguments": { + "collection": "test", + "timeseries": { + "timeField": "time", + "bucketMaxSpanSeconds": 3600, + "bucketRoundingSeconds": 3600 + } + } + }, + { + "name": "assertCollectionExists", + "object": "testRunner", + "arguments": { + "databaseName": "ts-tests", + "collectionName": "test" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "drop": "test" + }, + "databaseName": "ts-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "create": "test", + "timeseries": { + "timeField": "time", + "bucketMaxSpanSeconds": 3600, + "bucketRoundingSeconds": 3600 + } + }, + "databaseName": "ts-tests" + } + } + ] + } + ] } ] } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java index c162039c6b8..b99710eec7e 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java @@ -1202,6 +1202,12 @@ private TimeSeriesOptions createTimeSeriesOptions(final BsonDocument timeSeriesD case "metaField": options.metaField(cur.getValue().asString().getValue()); break; + case "bucketMaxSpanSeconds": + options.bucketMaxSpan(cur.getValue().asInt32().longValue(), TimeUnit.SECONDS); + break; + case "bucketRoundingSeconds": + options.bucketRounding(cur.getValue().asInt32().longValue(), TimeUnit.SECONDS); + break; case "granularity": options.granularity(createTimeSeriesGranularity(cur.getValue().asString().getValue())); break; From 91af5e2ac69429a5a4a4dacfb3305906f2e28762 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 6 Jun 2023 13:33:39 -0700 Subject: [PATCH 2/8] Add expected class name to the tests. JAVA-4888 --- .../mongodb/kotlin/client/coroutine/ExtensionMethodsTest.kt | 3 ++- .../kotlin/com/mongodb/kotlin/client/ExtensionMethodsTest.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/driver-kotlin-coroutine/src/test/kotlin/com/mongodb/kotlin/client/coroutine/ExtensionMethodsTest.kt b/driver-kotlin-coroutine/src/test/kotlin/com/mongodb/kotlin/client/coroutine/ExtensionMethodsTest.kt index 39780fb8d6c..9243748f1af 100644 --- a/driver-kotlin-coroutine/src/test/kotlin/com/mongodb/kotlin/client/coroutine/ExtensionMethodsTest.kt +++ b/driver-kotlin-coroutine/src/test/kotlin/com/mongodb/kotlin/client/coroutine/ExtensionMethodsTest.kt @@ -35,7 +35,8 @@ class ExtensionMethodsTest { "FindOneAndReplaceOptions", "FindOneAndUpdateOptions", "IndexOptions", - "TransactionOptions") + "TransactionOptions", + "TimeSeriesOptions") ClassGraph().enableClassInfo().enableMethodInfo().acceptPackages("com.mongodb").scan().use { scanResult -> val optionsClassesWithTimeUnit = diff --git a/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/ExtensionMethodsTest.kt b/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/ExtensionMethodsTest.kt index ad8571e5696..f0e7698124b 100644 --- a/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/ExtensionMethodsTest.kt +++ b/driver-kotlin-sync/src/test/kotlin/com/mongodb/kotlin/client/ExtensionMethodsTest.kt @@ -35,7 +35,8 @@ class ExtensionMethodsTest { "FindOneAndReplaceOptions", "FindOneAndUpdateOptions", "IndexOptions", - "TransactionOptions") + "TransactionOptions", + "TimeSeriesOptions") ClassGraph().enableClassInfo().enableMethodInfo().acceptPackages("com.mongodb").scan().use { scanResult -> val optionsClassesWithTimeUnit = From 132a840fced6b0ed2fb227310bc426ae3502c8ff Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Tue, 6 Jun 2023 13:51:09 -0700 Subject: [PATCH 3/8] Change javadoc. JAVA-4888 --- .../client/model/TimeSeriesOptions.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index d144d18ff29..aad59a76d41 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -114,10 +114,10 @@ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granu * * @param timeUnit the time unit. * @return time span between measurements. + * @since 4.10 * @mongodb.server.release 6.3 * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #bucketMaxSpan(Long, TimeUnit) - * @since 4.10 */ @Nullable public Long getBucketMaxSpan(final TimeUnit timeUnit) { @@ -131,17 +131,17 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { * Sets the maximum time span between measurements in a bucket. *

* The value of {@code bucketMaxSpanSeconds} must be the same as {@code bucketRoundingSeconds}. - * If you set the bucketMaxSpanSeconds, parameter, you can't set the granularity parameter + * If you set the {@code bucketMaxSpanSeconds}, parameter, you can't set the granularity parameter. *

* - * @param bucketMaxSpan - time span between measurements. After conversion to seconds using - * {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, the value must be >= 1. - * @param timeUnit - the time unit. + * @param bucketMaxSpan time span between measurements. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, + * the value must be >= 1. + * @param timeUnit the time unit. * @return this + * @since 4.10 * @mongodb.server.release 6.3 * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #getBucketMaxSpan(TimeUnit) - * @since 4.10 */ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final TimeUnit timeUnit) { if (bucketMaxSpan == null) { @@ -158,10 +158,10 @@ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final * * @param timeUnit the time unit. * @return the time interval. + * @since 4.10 * @mongodb.server.release 6.3 * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #bucketRounding(Long, TimeUnit) - * @since 4.10 */ @Nullable public Long getBucketRounding(final TimeUnit timeUnit) { @@ -174,18 +174,18 @@ public Long getBucketRounding(final TimeUnit timeUnit) { /** * Specifies the time interval that determines the starting timestamp for a new bucket. *

- * The value of {@code bucketRounding} must be the same as {@code bucketMaxSpan}. If you set the {@code bucketRounding}, parameter, - * you can't set the granularity parameter. + * The value of {@code bucketRoundingSeconds} must be the same as {@code bucketMaxSpanSeconds}. + * If you set the {@code bucketRoundingSeconds}, parameter, you can't set the granularity parameter. *

* - * @param bucketRounding - time interval. After conversion to seconds using - * {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, the value must be >= 1. - * @param timeUnit - the time unit. + * @param bucketRounding time interval. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, + * the value must be >= 1. + * @param timeUnit the time unit. * @return this + * @since 4.10 * @mongodb.server.release 6.3 * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #getBucketRounding(TimeUnit) - * @since 4.10 */ public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, final TimeUnit timeUnit) { if (bucketRounding == null) { From 2a86297b38937b076d1bc38270cc4624b5e62a02 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 7 Jun 2023 22:35:22 -0700 Subject: [PATCH 4/8] Update Java doc to clarify null return value. JAVA-4888 --- .../main/com/mongodb/client/model/TimeSeriesOptions.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index aad59a76d41..29e68f685b2 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -113,7 +113,7 @@ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granu * Returns the maximum time span between measurements in a bucket. * * @param timeUnit the time unit. - * @return time span between measurements. + * @return time span between measurements, or {@code null} if not set. * @since 4.10 * @mongodb.server.release 6.3 * @mongodb.driver.manual core/timeseries-collections/ Time-series collections @@ -135,7 +135,7 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { *

* * @param bucketMaxSpan time span between measurements. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, - * the value must be >= 1. + * the value must be >= 1. {@code null} can be provided to unset any previously set value. * @param timeUnit the time unit. * @return this * @since 4.10 @@ -157,7 +157,7 @@ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final * Returns the time interval that determines the starting timestamp for a new bucket. * * @param timeUnit the time unit. - * @return the time interval. + * @return the time interval, or {@code null} if not set. * @since 4.10 * @mongodb.server.release 6.3 * @mongodb.driver.manual core/timeseries-collections/ Time-series collections @@ -179,7 +179,7 @@ public Long getBucketRounding(final TimeUnit timeUnit) { *

* * @param bucketRounding time interval. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, - * the value must be >= 1. + * the value must be >= 1. {@code null} can be provided to unset any previously set value. * @param timeUnit the time unit. * @return this * @since 4.10 From d874af08e330bdcb427c96895eb2b28e069f37d9 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 14 Jun 2023 11:53:10 -0700 Subject: [PATCH 5/8] - Change javadoc. - Add tests. - Lower server version because bucket options were backported to 6.3 JAVA-4888 --- .../client/model/TimeSeriesGranularity.java | 2 +- .../client/model/TimeSeriesOptions.java | 26 +++++--- .../client/model/TimeSeriesOptionsTest.java | 65 +++++++++++++++++++ .../timeseries-collection.json | 2 +- 4 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesGranularity.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesGranularity.java index c15ba59b97a..266ef8489a0 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesGranularity.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesGranularity.java @@ -29,7 +29,7 @@ public enum TimeSeriesGranularity { /** * Seconds-level granularity. *

- * If granularity of a time-series collection is unspecified, this is the default value. + * This is the default value. *

*/ SECONDS, diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index 29e68f685b2..43a4ec35040 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; +import static com.mongodb.assertions.Assertions.isTrue; import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; @@ -97,7 +98,8 @@ public TimeSeriesGranularity getGranularity() { /** * Sets the granularity of the time-series data. *

- * The default value is {@link TimeSeriesGranularity#SECONDS}. + * The default value is {@link TimeSeriesGranularity#SECONDS} if neither {@code bucketMaxSpan} nor {@code bucketRounding} is set. + * If any of these bucketing options are set, the granularity parameter cannot be set. *

* * @param granularity the time-series granularity @@ -105,6 +107,8 @@ public TimeSeriesGranularity getGranularity() { * @see #getGranularity() */ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granularity) { + isTrue("granularity is not allowed when bucketMaxSpan is set", bucketMaxSpanSeconds == null); + isTrue("granularity is not allowed when bucketRounding is set", bucketRoundingSeconds == null); this.granularity = granularity; return this; } @@ -116,7 +120,6 @@ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granu * @return time span between measurements, or {@code null} if not set. * @since 4.10 * @mongodb.server.release 6.3 - * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #bucketMaxSpan(Long, TimeUnit) */ @Nullable @@ -130,8 +133,9 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { /** * Sets the maximum time span between measurements in a bucket. *

- * The value of {@code bucketMaxSpanSeconds} must be the same as {@code bucketRoundingSeconds}. - * If you set the {@code bucketMaxSpanSeconds}, parameter, you can't set the granularity parameter. + * The value of {@code bucketMaxSpan} must be the same as {@code bucketRounding}, + * which also means that the options must either be both set or both unset. + * If you set the {@code bucketMaxSpan}, parameter, you can't set the granularity parameter. *

* * @param bucketMaxSpan time span between measurements. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, @@ -140,13 +144,14 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { * @return this * @since 4.10 * @mongodb.server.release 6.3 - * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #getBucketMaxSpan(TimeUnit) */ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final TimeUnit timeUnit) { + notNull("timeUnit", timeUnit); if (bucketMaxSpan == null) { this.bucketMaxSpanSeconds = null; } else { + isTrue("bucketMaxSpan is not allowed when granularity is set", granularity == null); this.bucketMaxSpanSeconds = TimeUnit.SECONDS.convert(bucketMaxSpan, timeUnit); isTrueArgument("bucketMaxSpan, after conversion to seconds, must be >= 1", bucketMaxSpanSeconds > 0); } @@ -160,7 +165,6 @@ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final * @return the time interval, or {@code null} if not set. * @since 4.10 * @mongodb.server.release 6.3 - * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #bucketRounding(Long, TimeUnit) */ @Nullable @@ -174,8 +178,9 @@ public Long getBucketRounding(final TimeUnit timeUnit) { /** * Specifies the time interval that determines the starting timestamp for a new bucket. *

- * The value of {@code bucketRoundingSeconds} must be the same as {@code bucketMaxSpanSeconds}. - * If you set the {@code bucketRoundingSeconds}, parameter, you can't set the granularity parameter. + * The value of {@code bucketRounding} must be the same as {@code bucketMaxSpan}, + * which also means that the options must either be both set or both unset. + * If you set the {@code bucketRounding}, parameter, you can't set the granularity parameter. *

* * @param bucketRounding time interval. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, @@ -184,15 +189,16 @@ public Long getBucketRounding(final TimeUnit timeUnit) { * @return this * @since 4.10 * @mongodb.server.release 6.3 - * @mongodb.driver.manual core/timeseries-collections/ Time-series collections * @see #getBucketRounding(TimeUnit) */ public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, final TimeUnit timeUnit) { + notNull("timeUnit", timeUnit); if (bucketRounding == null) { this.bucketRoundingSeconds = null; } else { + isTrue("bucketRounding is not allowed when granularity is set", granularity == null); this.bucketRoundingSeconds = TimeUnit.SECONDS.convert(bucketRounding, timeUnit); - isTrueArgument("bucketRounding, after conversion to seconds, must be >= 1", bucketMaxSpanSeconds > 0); + isTrueArgument("bucketRounding, after conversion to seconds, must be >= 1", bucketRoundingSeconds > 0); } return this; } diff --git a/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java b/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java new file mode 100644 index 00000000000..a4f4f23a9f1 --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java @@ -0,0 +1,65 @@ +/* + * 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; + +import com.mongodb.lang.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class TimeSeriesOptionsTest { + + private TimeSeriesOptions timeSeriesOptions; + + @BeforeEach + void setUp() { + timeSeriesOptions = new TimeSeriesOptions("test"); + } + + @Test + void shouldThrowErrorWhenGranularityIsAlreadySet() { + //given + timeSeriesOptions.granularity(TimeSeriesGranularity.SECONDS); + + //when & then + assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketRounding(1L, TimeUnit.SECONDS)); + assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketMaxSpan(1L, TimeUnit.SECONDS)); + } + + @ParameterizedTest + @MethodSource("args") + void shouldThrowErrorWhenInvalidArgumentProvided(@Nullable final Long valueToSet, @Nullable final TimeUnit timeUnit) { + assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketRounding(valueToSet, timeUnit)); + assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketMaxSpan(valueToSet, timeUnit)); + } + + private static Stream args() { + return Stream.of( + arguments(1L, null), + arguments(null, null), + arguments(1L, TimeUnit.MILLISECONDS) + ); + } +} diff --git a/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json b/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json index 8525056fd1a..2ee52eac411 100644 --- a/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json +++ b/driver-core/src/test/resources/unified-test-format/collection-management/timeseries-collection.json @@ -255,7 +255,7 @@ "description": "createCollection with bucketing options", "runOnRequirements": [ { - "minServerVersion": "7.0" + "minServerVersion": "6.3" } ], "operations": [ From 85c85d2e946136cbacdf29f96e2a1ea6cf9e53e1 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 14 Jun 2023 12:12:44 -0700 Subject: [PATCH 6/8] Add null checks. JAVA-4888 --- .../main/com/mongodb/client/model/TimeSeriesOptions.java | 7 +++++-- .../com/mongodb/client/model/TimeSeriesOptionsTest.java | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index 43a4ec35040..6d55789d2ef 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -32,6 +32,7 @@ * @mongodb.driver.manual core/timeseries-collections/ Time-series collections */ public final class TimeSeriesOptions { + private static final String PARAMETER_TIME_UNIT = "timeUnit"; private final String timeField; private String metaField; private TimeSeriesGranularity granularity; @@ -124,6 +125,7 @@ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granu */ @Nullable public Long getBucketMaxSpan(final TimeUnit timeUnit) { + notNull(PARAMETER_TIME_UNIT, timeUnit); if (bucketMaxSpanSeconds == null) { return null; } @@ -147,7 +149,7 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { * @see #getBucketMaxSpan(TimeUnit) */ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final TimeUnit timeUnit) { - notNull("timeUnit", timeUnit); + notNull(PARAMETER_TIME_UNIT, timeUnit); if (bucketMaxSpan == null) { this.bucketMaxSpanSeconds = null; } else { @@ -169,6 +171,7 @@ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final */ @Nullable public Long getBucketRounding(final TimeUnit timeUnit) { + notNull(PARAMETER_TIME_UNIT, timeUnit); if (bucketRoundingSeconds == null) { return null; } @@ -192,7 +195,7 @@ public Long getBucketRounding(final TimeUnit timeUnit) { * @see #getBucketRounding(TimeUnit) */ public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, final TimeUnit timeUnit) { - notNull("timeUnit", timeUnit); + notNull(PARAMETER_TIME_UNIT, timeUnit); if (bucketRounding == null) { this.bucketRoundingSeconds = null; } else { diff --git a/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java b/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java index a4f4f23a9f1..5fed979ae5d 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java @@ -48,6 +48,12 @@ void shouldThrowErrorWhenGranularityIsAlreadySet() { assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketMaxSpan(1L, TimeUnit.SECONDS)); } + @Test + void shouldThrowErrorWhenGetWithNullParameter() { + assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketMaxSpan(null)); + assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketRounding(null)); + } + @ParameterizedTest @MethodSource("args") void shouldThrowErrorWhenInvalidArgumentProvided(@Nullable final Long valueToSet, @Nullable final TimeUnit timeUnit) { From 48f2189fe6af9ee53dfcd7aad3e7e6a58af00075 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 14 Jun 2023 14:51:02 -0700 Subject: [PATCH 7/8] Fix unnecessary assignment to variable. JAVA-4888 --- .../com/mongodb/client/model/TimeSeriesOptions.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index 6d55789d2ef..93ecf895b10 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -154,8 +154,9 @@ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final this.bucketMaxSpanSeconds = null; } else { isTrue("bucketMaxSpan is not allowed when granularity is set", granularity == null); - this.bucketMaxSpanSeconds = TimeUnit.SECONDS.convert(bucketMaxSpan, timeUnit); - isTrueArgument("bucketMaxSpan, after conversion to seconds, must be >= 1", bucketMaxSpanSeconds > 0); + long seconds = TimeUnit.SECONDS.convert(bucketMaxSpan, timeUnit); + isTrueArgument("bucketMaxSpan, after conversion to seconds, must be >= 1", seconds > 0); + this.bucketMaxSpanSeconds = seconds; } return this; } @@ -200,8 +201,9 @@ public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, fin this.bucketRoundingSeconds = null; } else { isTrue("bucketRounding is not allowed when granularity is set", granularity == null); - this.bucketRoundingSeconds = TimeUnit.SECONDS.convert(bucketRounding, timeUnit); - isTrueArgument("bucketRounding, after conversion to seconds, must be >= 1", bucketRoundingSeconds > 0); + long seconds = TimeUnit.SECONDS.convert(bucketRounding, timeUnit); + isTrueArgument("bucketRounding, after conversion to seconds, must be >= 1", seconds > 0); + this.bucketRoundingSeconds = seconds; } return this; } From 698f599fc28e64a226afe38a60b1a1eecac62286 Mon Sep 17 00:00:00 2001 From: "slav.babanin" Date: Wed, 14 Jun 2023 18:13:21 -0700 Subject: [PATCH 8/8] Correct javadoc. JAVA-4888 --- .../client/model/TimeSeriesOptions.java | 23 ++++++++----------- .../client/model/TimeSeriesOptionsTest.java | 19 ++++++++++----- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java index 93ecf895b10..6844e13848a 100644 --- a/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/TimeSeriesOptions.java @@ -32,7 +32,6 @@ * @mongodb.driver.manual core/timeseries-collections/ Time-series collections */ public final class TimeSeriesOptions { - private static final String PARAMETER_TIME_UNIT = "timeUnit"; private final String timeField; private String metaField; private TimeSeriesGranularity granularity; @@ -99,8 +98,8 @@ public TimeSeriesGranularity getGranularity() { /** * Sets the granularity of the time-series data. *

- * The default value is {@link TimeSeriesGranularity#SECONDS} if neither {@code bucketMaxSpan} nor {@code bucketRounding} is set. - * If any of these bucketing options are set, the granularity parameter cannot be set. + * The default value is {@link TimeSeriesGranularity#SECONDS} if neither {@link #bucketMaxSpan(Long, TimeUnit)} nor + * {@link #bucketRounding(Long, TimeUnit)} is set. If any of these bucketing options are set, the granularity parameter cannot be set. *

* * @param granularity the time-series granularity @@ -125,7 +124,7 @@ public TimeSeriesOptions granularity(@Nullable final TimeSeriesGranularity granu */ @Nullable public Long getBucketMaxSpan(final TimeUnit timeUnit) { - notNull(PARAMETER_TIME_UNIT, timeUnit); + notNull("timeUnit", timeUnit); if (bucketMaxSpanSeconds == null) { return null; } @@ -135,9 +134,8 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { /** * Sets the maximum time span between measurements in a bucket. *

- * The value of {@code bucketMaxSpan} must be the same as {@code bucketRounding}, - * which also means that the options must either be both set or both unset. - * If you set the {@code bucketMaxSpan}, parameter, you can't set the granularity parameter. + * The value of {@code bucketMaxSpan} must be the same as {@link #bucketRounding(Long, TimeUnit)}, which also means that the options + * must either be both set or both unset. If you set the {@code bucketMaxSpan} parameter, you can't set the granularity parameter. *

* * @param bucketMaxSpan time span between measurements. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, @@ -149,7 +147,7 @@ public Long getBucketMaxSpan(final TimeUnit timeUnit) { * @see #getBucketMaxSpan(TimeUnit) */ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final TimeUnit timeUnit) { - notNull(PARAMETER_TIME_UNIT, timeUnit); + notNull("timeUnit", timeUnit); if (bucketMaxSpan == null) { this.bucketMaxSpanSeconds = null; } else { @@ -172,7 +170,7 @@ public TimeSeriesOptions bucketMaxSpan(@Nullable final Long bucketMaxSpan, final */ @Nullable public Long getBucketRounding(final TimeUnit timeUnit) { - notNull(PARAMETER_TIME_UNIT, timeUnit); + notNull("timeUnit", timeUnit); if (bucketRoundingSeconds == null) { return null; } @@ -182,9 +180,8 @@ public Long getBucketRounding(final TimeUnit timeUnit) { /** * Specifies the time interval that determines the starting timestamp for a new bucket. *

- * The value of {@code bucketRounding} must be the same as {@code bucketMaxSpan}, - * which also means that the options must either be both set or both unset. - * If you set the {@code bucketRounding}, parameter, you can't set the granularity parameter. + * The value of {@code bucketRounding} must be the same as {@link #bucketMaxSpan(Long, TimeUnit)}, which also means that the options + * must either be both set or both unset. If you set the {@code bucketRounding} parameter, you can't set the granularity parameter. *

* * @param bucketRounding time interval. After conversion to seconds using {@link TimeUnit#convert(long, java.util.concurrent.TimeUnit)}, @@ -196,7 +193,7 @@ public Long getBucketRounding(final TimeUnit timeUnit) { * @see #getBucketRounding(TimeUnit) */ public TimeSeriesOptions bucketRounding(@Nullable final Long bucketRounding, final TimeUnit timeUnit) { - notNull(PARAMETER_TIME_UNIT, timeUnit); + notNull("timeUnit", timeUnit); if (bucketRounding == null) { this.bucketRoundingSeconds = null; } else { diff --git a/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java b/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java index 5fed979ae5d..4f38da79a3d 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/TimeSeriesOptionsTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -44,21 +45,27 @@ void shouldThrowErrorWhenGranularityIsAlreadySet() { timeSeriesOptions.granularity(TimeSeriesGranularity.SECONDS); //when & then - assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketRounding(1L, TimeUnit.SECONDS)); - assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketMaxSpan(1L, TimeUnit.SECONDS)); + assertAll( + () -> assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketRounding(1L, TimeUnit.SECONDS)), + () -> assertThrows(IllegalStateException.class, () -> timeSeriesOptions.bucketMaxSpan(1L, TimeUnit.SECONDS)) + ); } @Test void shouldThrowErrorWhenGetWithNullParameter() { - assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketMaxSpan(null)); - assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketRounding(null)); + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketMaxSpan(null)), + () -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.getBucketRounding(null)) + ); } @ParameterizedTest @MethodSource("args") void shouldThrowErrorWhenInvalidArgumentProvided(@Nullable final Long valueToSet, @Nullable final TimeUnit timeUnit) { - assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketRounding(valueToSet, timeUnit)); - assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketMaxSpan(valueToSet, timeUnit)); + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketRounding(valueToSet, timeUnit)), + () -> assertThrows(IllegalArgumentException.class, () -> timeSeriesOptions.bucketMaxSpan(valueToSet, timeUnit)) + ); } private static Stream args() {