Skip to content

Commit 479d213

Browse files
HyunSangHanchristophstrobl
authored andcommitted
Add time series collection support to $out aggregation operation.
`$out` operation stage now supports creating time series collections with configurable time field, metadata field, and granularity options. Closes: #4985 Original Pull Request: #4995 Signed-off-by: Hyunsang Han <[email protected]>
1 parent e22a0d2 commit 479d213

File tree

3 files changed

+218
-6
lines changed

3 files changed

+218
-6
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222

2323
import org.bson.Document;
2424
import org.bson.conversions.Bson;
25+
import org.jspecify.annotations.Nullable;
2526
import org.springframework.data.domain.Sort;
2627
import org.springframework.data.domain.Sort.Direction;
28+
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
2729
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder;
2830
import org.springframework.data.mongodb.core.aggregation.CountOperation.CountOperationBuilder;
2931
import org.springframework.data.mongodb.core.aggregation.FacetOperation.FacetOperationBuilder;
@@ -37,6 +39,7 @@
3739
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
3840
import org.springframework.data.mongodb.core.query.NearQuery;
3941
import org.springframework.data.mongodb.core.query.SerializationUtils;
42+
import org.springframework.data.mongodb.core.timeseries.Granularity;
4043
import org.springframework.util.Assert;
4144

4245
/**
@@ -53,6 +56,7 @@
5356
* @author Gustavo de Geus
5457
* @author Jérôme Guyon
5558
* @author Sangyong Choi
59+
* @author Hyunsang Han
5660
* @since 1.3
5761
*/
5862
public class Aggregation {
@@ -586,6 +590,46 @@ public static OutOperation out(String outCollectionName) {
586590
return new OutOperation(outCollectionName);
587591
}
588592

593+
/**
594+
* Creates a new {@link OutOperation} for time series collections using the given collection name and time series
595+
* options.
596+
*
597+
* @param outCollectionName collection name to export aggregation results. Must not be {@literal null}.
598+
* @param timeSeriesOptions must not be {@literal null}.
599+
* @return new instance of {@link OutOperation}.
600+
* @since 5.0
601+
*/
602+
public static OutOperation out(String outCollectionName, TimeSeriesOptions timeSeriesOptions) {
603+
return new OutOperation(outCollectionName).timeSeries(timeSeriesOptions);
604+
}
605+
606+
/**
607+
* Creates a new {@link OutOperation} for time series collections using the given collection name and time field.
608+
*
609+
* @param outCollectionName collection name to export aggregation results. Must not be {@literal null}.
610+
* @param timeField must not be {@literal null} or empty.
611+
* @return new instance of {@link OutOperation}.
612+
* @since 5.0
613+
*/
614+
public static OutOperation out(String outCollectionName, String timeField) {
615+
return new OutOperation(outCollectionName).timeSeries(timeField);
616+
}
617+
618+
/**
619+
* Creates a new {@link OutOperation} for time series collections using the given collection name, time field, meta
620+
* field, and granularity.
621+
*
622+
* @param outCollectionName collection name to export aggregation results. Must not be {@literal null}.
623+
* @param timeField must not be {@literal null} or empty.
624+
* @param metaField can be {@literal null}.
625+
* @param granularity can be {@literal null}.
626+
* @return new instance of {@link OutOperation}.
627+
* @since 5.0
628+
*/
629+
public static OutOperation out(String outCollectionName, String timeField, @Nullable String metaField, @Nullable Granularity granularity) {
630+
return new OutOperation(outCollectionName).timeSeries(timeField, metaField, granularity);
631+
}
632+
589633
/**
590634
* Creates a new {@link BucketOperation} given {@literal groupByField}.
591635
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import org.bson.Document;
1919
import org.jspecify.annotations.Nullable;
2020

21+
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
22+
import org.springframework.data.mongodb.core.timeseries.Granularity;
2123
import org.springframework.lang.Contract;
2224
import org.springframework.util.Assert;
2325
import org.springframework.util.StringUtils;
@@ -30,32 +32,36 @@
3032
*
3133
* @author Nikolay Bogdanov
3234
* @author Christoph Strobl
35+
* @author Hyunsang Han
3336
* @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/out/">MongoDB Aggregation Framework:
3437
* $out</a>
3538
*/
3639
public class OutOperation implements AggregationOperation {
3740

3841
private final @Nullable String databaseName;
3942
private final String collectionName;
43+
private final @Nullable TimeSeriesOptions timeSeriesOptions;
4044

4145
/**
4246
* @param outCollectionName Collection name to export the results. Must not be {@literal null}.
4347
*/
4448
public OutOperation(String outCollectionName) {
45-
this(null, outCollectionName);
49+
this(null, outCollectionName, null);
4650
}
4751

4852
/**
4953
* @param databaseName Optional database name the target collection is located in. Can be {@literal null}.
5054
* @param collectionName Collection name to export the results. Must not be {@literal null}. Can be {@literal null}.
51-
* @since 2.2
55+
* @param timeSeriesOptions Optional time series options for creating a time series collection. Can be {@literal null}.
56+
* @since 5.0
5257
*/
53-
private OutOperation(@Nullable String databaseName, String collectionName) {
58+
private OutOperation(@Nullable String databaseName, String collectionName, @Nullable TimeSeriesOptions timeSeriesOptions) {
5459

5560
Assert.notNull(collectionName, "Collection name must not be null");
5661

5762
this.databaseName = databaseName;
5863
this.collectionName = collectionName;
64+
this.timeSeriesOptions = timeSeriesOptions;
5965
}
6066

6167
/**
@@ -68,17 +74,81 @@ private OutOperation(@Nullable String databaseName, String collectionName) {
6874
*/
6975
@Contract("_ -> new")
7076
public OutOperation in(@Nullable String database) {
71-
return new OutOperation(database, collectionName);
77+
return new OutOperation(database, collectionName, timeSeriesOptions);
78+
}
79+
80+
/**
81+
* Set the time series options for creating a time series collection.
82+
*
83+
* @param timeSeriesOptions must not be {@literal null}.
84+
* @return new instance of {@link OutOperation}.
85+
* @since 5.0
86+
*/
87+
@Contract("_ -> new")
88+
public OutOperation timeSeries(TimeSeriesOptions timeSeriesOptions) {
89+
90+
Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
91+
return new OutOperation(databaseName, collectionName, timeSeriesOptions);
92+
}
93+
94+
/**
95+
* Set the time series options for creating a time series collection with only the time field.
96+
*
97+
* @param timeField must not be {@literal null} or empty.
98+
* @return new instance of {@link OutOperation}.
99+
* @since 5.0
100+
*/
101+
@Contract("_ -> new")
102+
public OutOperation timeSeries(String timeField) {
103+
104+
Assert.hasText(timeField, "TimeField must not be null or empty");
105+
return timeSeries(TimeSeriesOptions.timeSeries(timeField));
106+
}
107+
108+
/**
109+
* Set the time series options for creating a time series collection with time field, meta field, and granularity.
110+
*
111+
* @param timeField must not be {@literal null} or empty.
112+
* @param metaField can be {@literal null}.
113+
* @param granularity can be {@literal null}.
114+
* @return new instance of {@link OutOperation}.
115+
* @since 5.0
116+
*/
117+
@Contract("_, _, _ -> new")
118+
public OutOperation timeSeries(String timeField, @Nullable String metaField, @Nullable Granularity granularity) {
119+
120+
Assert.hasText(timeField, "TimeField must not be null or empty");
121+
return timeSeries(TimeSeriesOptions.timeSeries(timeField).metaField(metaField).granularity(granularity));
72122
}
73123

74124
@Override
75125
public Document toDocument(AggregationOperationContext context) {
76126

77-
if (!StringUtils.hasText(databaseName)) {
127+
if (!StringUtils.hasText(databaseName) && timeSeriesOptions == null) {
78128
return new Document(getOperator(), collectionName);
79129
}
80130

81-
return new Document(getOperator(), new Document("db", databaseName).append("coll", collectionName));
131+
Document outDocument = new Document("coll", collectionName);
132+
133+
if (StringUtils.hasText(databaseName)) {
134+
outDocument.put("db", databaseName);
135+
}
136+
137+
if (timeSeriesOptions != null) {
138+
Document timeSeriesDoc = new Document("timeField", timeSeriesOptions.getTimeField());
139+
140+
if (StringUtils.hasText(timeSeriesOptions.getMetaField())) {
141+
timeSeriesDoc.put("metaField", timeSeriesOptions.getMetaField());
142+
}
143+
144+
if (timeSeriesOptions.getGranularity() != null && timeSeriesOptions.getGranularity() != Granularity.DEFAULT) {
145+
timeSeriesDoc.put("granularity", timeSeriesOptions.getGranularity().name().toLowerCase());
146+
}
147+
148+
outDocument.put("timeseries", timeSeriesDoc);
149+
}
150+
151+
return new Document(getOperator(), outDocument);
82152
}
83153

84154
@Override

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020

2121
import org.bson.Document;
2222
import org.junit.jupiter.api.Test;
23+
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
24+
import org.springframework.data.mongodb.core.timeseries.Granularity;
2325

2426
/**
2527
* Unit tests for {@link OutOperation}.
2628
*
2729
* @author Nikolay Bogdanov
2830
* @author Christoph Strobl
2931
* @author Mark Paluch
32+
* @author Hyunsang Han
3033
*/
3134
class OutOperationUnitTest {
3235

@@ -48,4 +51,99 @@ void shouldRenderDocument() {
4851
.containsEntry("$out.db", "database-2");
4952
}
5053

54+
@Test // GH-4985
55+
void shouldRenderTimeSeriesCollectionWithTimeFieldOnly() {
56+
57+
Document result = out("timeseries-col").timeSeries("timestamp").toDocument(Aggregation.DEFAULT_CONTEXT);
58+
59+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
60+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
61+
assertThat(result).doesNotContainKey("$out.timeseries.metaField");
62+
assertThat(result).doesNotContainKey("$out.timeseries.granularity");
63+
}
64+
65+
@Test // GH-4985
66+
void shouldRenderTimeSeriesCollectionWithAllOptions() {
67+
68+
Document result = out("timeseries-col").timeSeries("timestamp", "metadata", Granularity.SECONDS)
69+
.toDocument(Aggregation.DEFAULT_CONTEXT);
70+
71+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
72+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
73+
assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
74+
assertThat(result).containsEntry("$out.timeseries.granularity", "seconds");
75+
}
76+
77+
@Test // GH-4985
78+
void shouldRenderTimeSeriesCollectionWithDatabaseAndAllOptions() {
79+
80+
Document result = out("timeseries-col").in("test-db").timeSeries("timestamp", "metadata", Granularity.MINUTES)
81+
.toDocument(Aggregation.DEFAULT_CONTEXT);
82+
83+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
84+
assertThat(result).containsEntry("$out.db", "test-db");
85+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
86+
assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
87+
assertThat(result).containsEntry("$out.timeseries.granularity", "minutes");
88+
}
89+
90+
@Test // GH-4985
91+
void shouldRenderTimeSeriesCollectionWithTimeSeriesOptions() {
92+
93+
TimeSeriesOptions options = TimeSeriesOptions.timeSeries("timestamp").metaField("metadata").granularity(Granularity.HOURS);
94+
Document result = out("timeseries-col").timeSeries(options).toDocument(Aggregation.DEFAULT_CONTEXT);
95+
96+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
97+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
98+
assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
99+
assertThat(result).containsEntry("$out.timeseries.granularity", "hours");
100+
}
101+
102+
@Test // GH-4985
103+
void shouldRenderTimeSeriesCollectionWithPartialOptions() {
104+
105+
Document result = out("timeseries-col").timeSeries("timestamp", "metadata", null)
106+
.toDocument(Aggregation.DEFAULT_CONTEXT);
107+
108+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
109+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
110+
assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
111+
assertThat(result).doesNotContainKey("$out.timeseries.granularity");
112+
}
113+
114+
@Test // GH-4985
115+
void outWithTimeSeriesOptionsShouldRenderCorrectly() {
116+
117+
TimeSeriesOptions options = TimeSeriesOptions.timeSeries("timestamp").metaField("metadata").granularity(Granularity.SECONDS);
118+
Document result = Aggregation.out("timeseries-col", options).toDocument(Aggregation.DEFAULT_CONTEXT);
119+
120+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
121+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
122+
assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
123+
assertThat(result).containsEntry("$out.timeseries.granularity", "seconds");
124+
}
125+
126+
@Test // GH-4985
127+
void outWithTimeFieldOnlyShouldRenderCorrectly() {
128+
129+
Document result = Aggregation.out("timeseries-col", "timestamp").toDocument(Aggregation.DEFAULT_CONTEXT);
130+
131+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
132+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
133+
assertThat(result).doesNotContainKey("$out.timeseries.metaField");
134+
assertThat(result).doesNotContainKey("$out.timeseries.granularity");
135+
}
136+
137+
@Test // GH-4985
138+
void outWithAllOptionsShouldRenderCorrectly() {
139+
140+
Document result = Aggregation.out("timeseries-col", "timestamp", "metadata", Granularity.MINUTES)
141+
.toDocument(Aggregation.DEFAULT_CONTEXT);
142+
143+
assertThat(result).containsEntry("$out.coll", "timeseries-col");
144+
assertThat(result).containsEntry("$out.timeseries.timeField", "timestamp");
145+
assertThat(result).containsEntry("$out.timeseries.metaField", "metadata");
146+
assertThat(result).containsEntry("$out.timeseries.granularity", "minutes");
147+
}
148+
51149
}

0 commit comments

Comments
 (0)