diff --git a/pom.xml b/pom.xml
index 3daab4d790..c2cbbf7b60 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-4985-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index fc88571622..d51dc07dcf 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-4985-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 6f34da5660..ac0adc4269 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -13,7 +13,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-4985-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
index d7e47eac04..3ccf6fe4e8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
@@ -30,7 +30,6 @@
import org.bson.BsonNull;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
-
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty;
@@ -41,6 +40,7 @@
import org.springframework.data.mongodb.core.schema.QueryCharacteristic;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.timeseries.GranularityDefinition;
+import org.springframework.data.mongodb.core.timeseries.Span;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.util.Optionals;
import org.springframework.lang.CheckReturnValue;
@@ -982,16 +982,24 @@ public static class TimeSeriesOptions {
private @Nullable final String metaField;
private final GranularityDefinition granularity;
+ private final @Nullable Span span;
private final Duration expireAfter;
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity,
- Duration expireAfter) {
+ @Nullable Span span, Duration expireAfter) {
+
Assert.hasText(timeField, "Time field must not be empty or null");
+ if (!Granularity.DEFAULT.equals(granularity) && span != null) {
+ throw new IllegalArgumentException(
+ "Cannot use granularity [%s] in conjunction with span".formatted(granularity.name()));
+ }
+
this.timeField = timeField;
this.metaField = metaField;
this.granularity = granularity;
+ this.span = span;
this.expireAfter = expireAfter;
}
@@ -1004,7 +1012,7 @@ private TimeSeriesOptions(String timeField, @Nullable String metaField, Granular
* @return new instance of {@link TimeSeriesOptions}.
*/
public static TimeSeriesOptions timeSeries(String timeField) {
- return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, Duration.ofSeconds(-1));
+ return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, null, Duration.ofSeconds(-1));
}
/**
@@ -1018,7 +1026,7 @@ public static TimeSeriesOptions timeSeries(String timeField) {
*/
@Contract("_ -> new")
public TimeSeriesOptions metaField(String metaField) {
- return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter);
+ return new TimeSeriesOptions(timeField, metaField, granularity, span, expireAfter);
}
/**
@@ -1030,7 +1038,20 @@ public TimeSeriesOptions metaField(String metaField) {
*/
@Contract("_ -> new")
public TimeSeriesOptions granularity(GranularityDefinition granularity) {
- return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter);
+ return new TimeSeriesOptions(timeField, metaField, granularity, span, expireAfter);
+ }
+
+ /**
+ * Select the time between timestamps in the same bucket to define how data in the time series collection is
+ * organized. Cannot be used in conjunction with {@link #granularity(GranularityDefinition)}.
+ *
+ * @return new instance of {@link TimeSeriesOptions}.
+ * @see Span
+ * @since 5.0
+ */
+ @Contract("_ -> new")
+ public TimeSeriesOptions span(Span span) {
+ return new TimeSeriesOptions(timeField, metaField, granularity, span, expireAfter);
}
/**
@@ -1043,7 +1064,7 @@ public TimeSeriesOptions granularity(GranularityDefinition granularity) {
*/
@Contract("_ -> new")
public TimeSeriesOptions expireAfter(Duration ttl) {
- return new TimeSeriesOptions(timeField, metaField, granularity, ttl);
+ return new TimeSeriesOptions(timeField, metaField, granularity, span, ttl);
}
/**
@@ -1079,11 +1100,21 @@ public Duration getExpireAfter() {
return expireAfter;
}
+ /**
+ * Get the span that defines a bucket.
+ *
+ * @return {@literal null} if not specified.
+ * @since 5.0
+ */
+ public @Nullable Span getSpan() {
+ return span;
+ }
+
@Override
public String toString() {
return "TimeSeriesOptions{" + "timeField='" + timeField + '\'' + ", metaField='" + metaField + '\''
- + ", granularity=" + granularity + '}';
+ + ", granularity=" + granularity + ", span=" + span + ", expireAfter=" + expireAfter + '}';
}
@Override
@@ -1103,6 +1134,13 @@ public boolean equals(@Nullable Object o) {
if (!ObjectUtils.nullSafeEquals(metaField, that.metaField)) {
return false;
}
+ if (!ObjectUtils.nullSafeEquals(span, that.span)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(expireAfter, that.expireAfter)) {
+ return false;
+ }
+
return ObjectUtils.nullSafeEquals(granularity, that.granularity);
}
@@ -1111,6 +1149,8 @@ public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(timeField);
result = 31 * result + ObjectUtils.nullSafeHashCode(metaField);
result = 31 * result + ObjectUtils.nullSafeHashCode(granularity);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(span);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(expireAfter);
return result;
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
index 1327656356..eab2dc59f9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
@@ -371,6 +371,13 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
if (!Granularity.DEFAULT.equals(it.getGranularity())) {
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
}
+ if (it.getSpan() != null) {
+
+ long bucketMaxSpanInSeconds = it.getSpan().time().toSeconds();
+ // right now there's only one value since the two options must have the same value.
+ options.bucketMaxSpan(bucketMaxSpanInSeconds, TimeUnit.SECONDS);
+ options.bucketRounding(bucketMaxSpanInSeconds, TimeUnit.SECONDS);
+ }
if (!it.getExpireAfter().isNegative()) {
result.expireAfter(it.getExpireAfter().toSeconds(), TimeUnit.SECONDS);
@@ -1131,7 +1138,7 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) {
if (StringUtils.hasText(source.getMetaField())) {
target = target.metaField(mappedNameOrDefault(source.getMetaField()));
}
- return target.granularity(source.getGranularity()).expireAfter(source.getExpireAfter());
+ return target.granularity(source.getGranularity()).expireAfter(source.getExpireAfter()).span(source.getSpan());
}
@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
index 45de38ed21..d82db54e9c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java
@@ -22,8 +22,10 @@
import org.bson.Document;
import org.bson.conversions.Bson;
+import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
+import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.CountOperation.CountOperationBuilder;
import org.springframework.data.mongodb.core.aggregation.FacetOperation.FacetOperationBuilder;
@@ -37,6 +39,7 @@
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.SerializationUtils;
+import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.util.Assert;
/**
@@ -53,6 +56,7 @@
* @author Gustavo de Geus
* @author Jérôme Guyon
* @author Sangyong Choi
+ * @author Hyunsang Han
* @since 1.3
*/
public class Aggregation {
@@ -586,6 +590,46 @@ public static OutOperation out(String outCollectionName) {
return new OutOperation(outCollectionName);
}
+ /**
+ * Creates a new {@link OutOperation} for time series collections using the given collection name and time series
+ * options.
+ *
+ * @param outCollectionName collection name to export aggregation results. Must not be {@literal null}.
+ * @param timeSeriesOptions must not be {@literal null}.
+ * @return new instance of {@link OutOperation}.
+ * @since 5.0
+ */
+ public static OutOperation out(String outCollectionName, TimeSeriesOptions timeSeriesOptions) {
+ return new OutOperation(outCollectionName).timeSeries(timeSeriesOptions);
+ }
+
+ /**
+ * Creates a new {@link OutOperation} for time series collections using the given collection name and time field.
+ *
+ * @param outCollectionName collection name to export aggregation results. Must not be {@literal null}.
+ * @param timeField must not be {@literal null} or empty.
+ * @return new instance of {@link OutOperation}.
+ * @since 5.0
+ */
+ public static OutOperation out(String outCollectionName, String timeField) {
+ return new OutOperation(outCollectionName).timeSeries(timeField);
+ }
+
+ /**
+ * Creates a new {@link OutOperation} for time series collections using the given collection name, time field, meta
+ * field, and granularity.
+ *
+ * @param outCollectionName collection name to export aggregation results. Must not be {@literal null}.
+ * @param timeField must not be {@literal null} or empty.
+ * @param metaField can be {@literal null}.
+ * @param granularity can be {@literal null}.
+ * @return new instance of {@link OutOperation}.
+ * @since 5.0
+ */
+ public static OutOperation out(String outCollectionName, String timeField, @Nullable String metaField, @Nullable Granularity granularity) {
+ return new OutOperation(outCollectionName).timeSeries(timeField, metaField, granularity);
+ }
+
/**
* Creates a new {@link BucketOperation} given {@literal groupByField}.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java
index 9d0ddf6d67..7867b3c0a0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java
@@ -17,7 +17,8 @@
import org.bson.Document;
import org.jspecify.annotations.Nullable;
-
+import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
+import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -30,6 +31,7 @@
*
* @author Nikolay Bogdanov
* @author Christoph Strobl
+ * @author Hyunsang Han
* @see MongoDB Aggregation Framework:
* $out
*/
@@ -37,25 +39,30 @@ public class OutOperation implements AggregationOperation {
private final @Nullable String databaseName;
private final String collectionName;
+ private final @Nullable TimeSeriesOptions timeSeriesOptions;
/**
* @param outCollectionName Collection name to export the results. Must not be {@literal null}.
*/
public OutOperation(String outCollectionName) {
- this(null, outCollectionName);
+ this(null, outCollectionName, null);
}
/**
* @param databaseName Optional database name the target collection is located in. Can be {@literal null}.
* @param collectionName Collection name to export the results. Must not be {@literal null}. Can be {@literal null}.
- * @since 2.2
+ * @param timeSeriesOptions Optional time series options for creating a time series collection. Can be
+ * {@literal null}.
+ * @since 5.0
*/
- private OutOperation(@Nullable String databaseName, String collectionName) {
+ private OutOperation(@Nullable String databaseName, String collectionName,
+ @Nullable TimeSeriesOptions timeSeriesOptions) {
Assert.notNull(collectionName, "Collection name must not be null");
this.databaseName = databaseName;
this.collectionName = collectionName;
+ this.timeSeriesOptions = timeSeriesOptions;
}
/**
@@ -68,17 +75,89 @@ private OutOperation(@Nullable String databaseName, String collectionName) {
*/
@Contract("_ -> new")
public OutOperation in(@Nullable String database) {
- return new OutOperation(database, collectionName);
+ return new OutOperation(database, collectionName, timeSeriesOptions);
+ }
+
+ /**
+ * Set the time series options for creating a time series collection.
+ *
+ * @param timeSeriesOptions must not be {@literal null}.
+ * @return new instance of {@link OutOperation}.
+ * @since 5.0
+ */
+ @Contract("_ -> new")
+ public OutOperation timeSeries(TimeSeriesOptions timeSeriesOptions) {
+
+ Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
+ return new OutOperation(databaseName, collectionName, timeSeriesOptions);
+ }
+
+ /**
+ * Set the time series options for creating a time series collection with only the time field.
+ *
+ * @param timeField must not be {@literal null} or empty.
+ * @return new instance of {@link OutOperation}.
+ * @since 5.0
+ */
+ @Contract("_ -> new")
+ public OutOperation timeSeries(String timeField) {
+
+ Assert.hasText(timeField, "TimeField must not be null or empty");
+ return timeSeries(TimeSeriesOptions.timeSeries(timeField));
+ }
+
+ /**
+ * Set the time series options for creating a time series collection with time field, meta field, and granularity.
+ *
+ * @param timeField must not be {@literal null} or empty.
+ * @param metaField can be {@literal null}.
+ * @param granularity can be {@literal null}.
+ * @return new instance of {@link OutOperation}.
+ * @since 5.0
+ */
+ @Contract("_, _, _ -> new")
+ public OutOperation timeSeries(String timeField, @Nullable String metaField, @Nullable Granularity granularity) {
+
+ Assert.hasText(timeField, "TimeField must not be null or empty");
+ return timeSeries(TimeSeriesOptions.timeSeries(timeField).metaField(metaField).granularity(granularity));
}
@Override
public Document toDocument(AggregationOperationContext context) {
- if (!StringUtils.hasText(databaseName)) {
+ if (!StringUtils.hasText(databaseName) && timeSeriesOptions == null) {
return new Document(getOperator(), collectionName);
}
- return new Document(getOperator(), new Document("db", databaseName).append("coll", collectionName));
+ Document outDocument = new Document("coll", collectionName);
+
+ if (StringUtils.hasText(databaseName)) {
+ outDocument.put("db", databaseName);
+ }
+
+ if (timeSeriesOptions != null) {
+
+ Document timeSeriesDoc = new Document("timeField", timeSeriesOptions.getTimeField());
+
+ if (StringUtils.hasText(timeSeriesOptions.getMetaField())) {
+ timeSeriesDoc.put("metaField", timeSeriesOptions.getMetaField());
+ }
+
+ if (timeSeriesOptions.getGranularity() != null && timeSeriesOptions.getGranularity() != Granularity.DEFAULT) {
+ timeSeriesDoc.put("granularity", timeSeriesOptions.getGranularity().name().toLowerCase());
+ }
+
+ if (timeSeriesOptions.getSpan() != null && timeSeriesOptions.getSpan().time() != null) {
+
+ long spanSeconds = timeSeriesOptions.getSpan().time().getSeconds();
+ timeSeriesDoc.put("bucketMaxSpanSeconds", spanSeconds);
+ timeSeriesDoc.put("bucketRoundingSeconds", spanSeconds);
+ }
+
+ outDocument.put("timeseries", timeSeriesDoc);
+ }
+
+ return new Document(getOperator(), outDocument);
}
@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Span.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Span.java
new file mode 100644
index 0000000000..597aea54ff
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Span.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2025-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.springframework.data.mongodb.core.timeseries;
+
+import java.time.Duration;
+
+/**
+ * @author Christoph Strobl
+ * @since 5.0
+ */
+public interface Span {
+
+ /**
+ * Defines the time between timestamps in the same bucket in a range between {@literal 1-31.536.000} seconds.
+ */
+ Duration time();
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
index fd8c9fb972..1586e272e1 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
@@ -16,6 +16,8 @@
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
import static org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
import static org.springframework.data.mongodb.core.CollectionOptions.emitChangedRevisions;
@@ -24,6 +26,7 @@
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable;
+import java.time.Duration;
import java.util.List;
import org.bson.BsonNull;
@@ -33,6 +36,7 @@
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.schema.QueryCharacteristics;
+import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
/**
@@ -79,6 +83,14 @@ void timeSeriesEquals() {
.isNotEqualTo(empty().timeSeries(TimeSeriesOptions.timeSeries("other")));
}
+ @Test // GH-4985
+ void timeSeriesValidatesGranularityAndSpanSettings() {
+
+ assertThatNoException().isThrownBy(() -> empty().timeSeries(TimeSeriesOptions.timeSeries("tf").span(() -> Duration.ofSeconds(1)).granularity(Granularity.DEFAULT)));
+ assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> TimeSeriesOptions.timeSeries("tf").granularity(Granularity.HOURS).span(() -> Duration.ofSeconds(1)));
+ assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> TimeSeriesOptions.timeSeries("tf").span(() -> Duration.ofSeconds(1)).granularity(Granularity.HOURS));
+ }
+
@Test // GH-4210
void validatorEquals() {
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java
index a86433175a..2433424d03 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java
@@ -18,8 +18,12 @@
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
+import java.time.Duration;
+
import org.bson.Document;
import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
+import org.springframework.data.mongodb.core.timeseries.Granularity;
/**
* Unit tests for {@link OutOperation}.
@@ -27,6 +31,7 @@
* @author Nikolay Bogdanov
* @author Christoph Strobl
* @author Mark Paluch
+ * @author Hyunsang Han
*/
class OutOperationUnitTest {
@@ -48,4 +53,113 @@ void shouldRenderDocument() {
.containsEntry("$out.db", "database-2");
}
+ @Test // GH-4985
+ void shouldRenderTimeSeriesCollectionWithTimeFieldOnly() {
+
+ Document result = out("timeseries-col").timeSeries("timestamp").toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).doesNotContainKey("$out.timeseries.metaField");
+ assertThat(result).doesNotContainKey("$out.timeseries.granularity");
+ }
+
+ @Test // GH-4985
+ void shouldRenderTimeSeriesCollectionWithAllOptions() {
+
+ Document result = out("timeseries-col").timeSeries("timestamp", "metadata", Granularity.SECONDS)
+ .toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).containsEntry("$out.timeseries.granularity", "seconds");
+ }
+
+ @Test // GH-4985
+ void shouldRenderTimeSeriesCollectionWithDatabaseAndAllOptions() {
+
+ Document result = out("timeseries-col").in("test-db").timeSeries("timestamp", "metadata", Granularity.MINUTES)
+ .toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.db", "test-db");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).containsEntry("$out.timeseries.granularity", "minutes");
+ }
+
+ @Test // GH-4985
+ void shouldRenderTimeSeriesCollectionWithTimeSeriesOptions() {
+
+ TimeSeriesOptions options = TimeSeriesOptions.timeSeries("timestamp").metaField("metadata").granularity(Granularity.HOURS);
+ Document result = out("timeseries-col").timeSeries(options).toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).containsEntry("$out.timeseries.granularity", "hours");
+ }
+
+ @Test // GH-4985
+ void shouldRenderTimeSeriesCollectionWithPartialOptions() {
+
+ Document result = out("timeseries-col").timeSeries("timestamp", "metadata", null)
+ .toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).doesNotContainKey("$out.timeseries.granularity");
+ }
+
+ @Test // GH-4985
+ void outWithTimeSeriesOptionsShouldRenderCorrectly() {
+
+ TimeSeriesOptions options = TimeSeriesOptions.timeSeries("timestamp").metaField("metadata").granularity(Granularity.SECONDS);
+ Document result = Aggregation.out("timeseries-col", options).toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).containsEntry("$out.timeseries.granularity", "seconds");
+ }
+
+ @Test // GH-4985
+ void outWithTimeSeriesOptionsUsingSpanShouldRenderCorrectly() {
+
+ TimeSeriesOptions options = TimeSeriesOptions.timeSeries("timestamp").metaField("metadata").span(() -> Duration.ofMinutes(2));
+ Document result = Aggregation.out("timeseries-col", options).toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).containsEntry("$out.timeseries.bucketMaxSpanSeconds", 120L);
+ assertThat(result).containsEntry("$out.timeseries.bucketRoundingSeconds", 120L);
+ assertThat(result).doesNotContainKey("$out.timeseries.granularity");
+ }
+
+ @Test // GH-4985
+ void outWithTimeFieldOnlyShouldRenderCorrectly() {
+
+ Document result = Aggregation.out("timeseries-col", "timestamp").toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).doesNotContainKey("$out.timeseries.metaField");
+ assertThat(result).doesNotContainKey("$out.timeseries.granularity");
+ }
+
+ @Test // GH-4985
+ void outWithAllOptionsShouldRenderCorrectly() {
+
+ Document result = Aggregation.out("timeseries-col", "timestamp", "metadata", Granularity.MINUTES)
+ .toDocument(Aggregation.DEFAULT_CONTEXT);
+
+ assertThat(result).containsEntry("$out.coll", "timeseries-col");
+ assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
+ assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
+ assertThat(result).containsEntry("$out.timeseries.granularity", "minutes");
+ }
+
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java
index aa069a2710..fe65d21084 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java
@@ -23,6 +23,7 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -150,7 +151,7 @@ void shouldDocumentAggregation() throws IOException {
assertThatJson(json).inPath("$.methods[?(@.name == 'findAllLastnames')].query").isArray().element(0).isObject()
.containsEntry("pipeline",
- "[{ '$match' : { 'last_name' : { '$ne' : null } } }, { '$project': { '_id' : '$last_name' } }]");
+ List.of("{ '$match' : { 'last_name' : { '$ne' : null } } }", "{ '$project': { '_id' : '$last_name' } }"));
}
@Test // GH-4964
@@ -165,7 +166,7 @@ void shouldDocumentPipelineUpdate() throws IOException {
assertThatJson(json).inPath("$.methods[?(@.name == 'findAndIncrementVisitsViaPipelineByLastname')].query").isArray()
.element(0).isObject().containsEntry("filter", "{'lastname':?0}").containsEntry("update-pipeline",
- "[{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }]");
+ List.of("{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }"));
}
@Test // GH-4964