Polishing.
Tweak wording in the docs. Remove unused code. Fix generics. Rename AggregateContext to AggregationOperation to AggregationDefinition to avoid yet another Context object. See #3542. Original pull request: #3545.
This commit is contained in:
@@ -67,7 +67,9 @@ class AggregationUtil {
|
||||
|
||||
AggregationOperationContext createAggregationContext(Aggregation aggregation, @Nullable Class<?> inputType) {
|
||||
|
||||
if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.NONE) {
|
||||
DomainTypeMapping domainTypeMapping = aggregation.getOptions().getDomainTypeMapping();
|
||||
|
||||
if (domainTypeMapping == DomainTypeMapping.NONE) {
|
||||
return Aggregation.DEFAULT_CONTEXT;
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ class AggregationUtil {
|
||||
return untypedMappingContext.get();
|
||||
}
|
||||
|
||||
if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.STRICT
|
||||
if (domainTypeMapping == DomainTypeMapping.STRICT
|
||||
&& !aggregation.getPipeline().containsUnionWith()) {
|
||||
return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
|
||||
}
|
||||
@@ -85,8 +87,8 @@ class AggregationUtil {
|
||||
return new RelaxedTypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
|
||||
}
|
||||
|
||||
inputType = ((TypedAggregation) aggregation).getInputType();
|
||||
if (aggregation.getOptions().getDomainTypeMapping() == DomainTypeMapping.STRICT
|
||||
inputType = ((TypedAggregation<?>) aggregation).getInputType();
|
||||
if (domainTypeMapping == DomainTypeMapping.STRICT
|
||||
&& !aggregation.getPipeline().containsUnionWith()) {
|
||||
return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper);
|
||||
}
|
||||
@@ -130,53 +132,6 @@ class AggregationUtil {
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code $count} aggregation for {@link Query} and optionally a {@link Class entity class}.
|
||||
*
|
||||
* @param query must not be {@literal null}.
|
||||
* @param entityClass can be {@literal null} if the {@link Query} object is empty.
|
||||
* @return the {@link Aggregation} pipeline definition to run a {@code $count} aggregation.
|
||||
*/
|
||||
Aggregation createCountAggregation(Query query, @Nullable Class<?> entityClass) {
|
||||
|
||||
List<AggregationOperation> pipeline = computeCountAggregationPipeline(query, entityClass);
|
||||
|
||||
Aggregation aggregation = entityClass != null ? Aggregation.newAggregation(entityClass, pipeline)
|
||||
: Aggregation.newAggregation(pipeline);
|
||||
aggregation.withOptions(AggregationOptions.builder().collation(query.getCollation().orElse(null)).build());
|
||||
|
||||
return aggregation;
|
||||
}
|
||||
|
||||
private List<AggregationOperation> computeCountAggregationPipeline(Query query, @Nullable Class<?> entityType) {
|
||||
|
||||
CountOperation count = Aggregation.count().as("totalEntityCount");
|
||||
if (query.getQueryObject().isEmpty()) {
|
||||
return Collections.singletonList(count);
|
||||
}
|
||||
|
||||
Assert.notNull(entityType, "Entity type must not be null!");
|
||||
|
||||
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(),
|
||||
mappingContext.getPersistentEntity(entityType));
|
||||
|
||||
CriteriaDefinition criteria = new CriteriaDefinition() {
|
||||
|
||||
@Override
|
||||
public Document getCriteriaObject() {
|
||||
return mappedQuery;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getKey() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return Arrays.asList(Aggregation.match(criteria), count);
|
||||
}
|
||||
|
||||
private List<Document> mapAggregationPipeline(List<Document> pipeline) {
|
||||
|
||||
return pipeline.stream().map(val -> queryMapper.getMappedObject(val, Optional.empty()))
|
||||
|
||||
@@ -55,7 +55,7 @@ import org.springframework.data.mongodb.SessionSynchronization;
|
||||
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
|
||||
import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext;
|
||||
import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.AggregateContext;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.CountContext;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.DeleteContext;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.DistinctQueryContext;
|
||||
@@ -1989,7 +1989,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
|
||||
public <O> AggregationResults<O> aggregate(Aggregation aggregation, Class<?> inputType, Class<O> outputType) {
|
||||
|
||||
return aggregate(aggregation, getCollectionName(inputType), outputType,
|
||||
queryOperations.createAggregationContext(aggregation, inputType).getAggregationOperationContext());
|
||||
queryOperations.createAggregation(aggregation, inputType).getAggregationOperationContext());
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
@@ -2096,11 +2096,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
|
||||
Assert.notNull(aggregation, "Aggregation pipeline must not be null!");
|
||||
Assert.notNull(outputType, "Output type must not be null!");
|
||||
|
||||
return doAggregate(aggregation, collectionName, outputType, queryOperations.createAggregationContext(aggregation, context));
|
||||
return doAggregate(aggregation, collectionName, outputType,
|
||||
queryOperations.createAggregation(aggregation, context));
|
||||
}
|
||||
|
||||
private <O> AggregationResults<O> doAggregate(Aggregation aggregation, String collectionName, Class<O> outputType,
|
||||
AggregateContext context) {
|
||||
AggregationDefinition context) {
|
||||
return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext());
|
||||
}
|
||||
|
||||
@@ -2189,10 +2190,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
|
||||
Assert.notNull(outputType, "Output type must not be null!");
|
||||
Assert.isTrue(!aggregation.getOptions().isExplain(), "Can't use explain option with streaming!");
|
||||
|
||||
AggregateContext aggregateContext = queryOperations.createAggregationContext(aggregation, context);
|
||||
AggregationDefinition aggregationDefinition = queryOperations.createAggregation(aggregation, context);
|
||||
|
||||
AggregationOptions options = aggregation.getOptions();
|
||||
List<Document> pipeline = aggregateContext.getAggregationPipeline();
|
||||
List<Document> pipeline = aggregationDefinition.getAggregationPipeline();
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(pipeline), collectionName);
|
||||
|
||||
@@ -200,33 +200,33 @@ class QueryOperations {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link AggregateContext} for the given {@link Aggregation}.
|
||||
* Create a new {@link AggregationDefinition} for the given {@link Aggregation}.
|
||||
*
|
||||
* @param aggregation must not be {@literal null}.
|
||||
* @param inputType fallback mapping type in case of untyped aggregation. Can be {@literal null}.
|
||||
* @return new instance of {@link AggregateContext}.
|
||||
* @return new instance of {@link AggregationDefinition}.
|
||||
* @since 3.2
|
||||
*/
|
||||
AggregateContext createAggregationContext(Aggregation aggregation, @Nullable Class<?> inputType) {
|
||||
return new AggregateContext(aggregation, inputType);
|
||||
AggregationDefinition createAggregation(Aggregation aggregation, @Nullable Class<?> inputType) {
|
||||
return new AggregationDefinition(aggregation, inputType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link AggregateContext} for the given {@link Aggregation}.
|
||||
* Create a new {@link AggregationDefinition} for the given {@link Aggregation}.
|
||||
*
|
||||
* @param aggregation must not be {@literal null}.
|
||||
* @param aggregationOperationContext the {@link AggregationOperationContext} to use. Can be {@literal null}.
|
||||
* @return new instance of {@link AggregateContext}.
|
||||
* @return new instance of {@link AggregationDefinition}.
|
||||
* @since 3.2
|
||||
*/
|
||||
AggregateContext createAggregationContext(Aggregation aggregation,
|
||||
AggregationDefinition createAggregation(Aggregation aggregation,
|
||||
@Nullable AggregationOperationContext aggregationOperationContext) {
|
||||
return new AggregateContext(aggregation, aggregationOperationContext);
|
||||
return new AggregationDefinition(aggregation, aggregationOperationContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link QueryContext} encapsulates common tasks required to convert a {@link Query} into its MongoDB document
|
||||
* representation, mapping fieldnames, as well as determinging and applying {@link Collation collations}.
|
||||
* representation, mapping field names, as well as determining and applying {@link Collation collations}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@@ -235,7 +235,7 @@ class QueryOperations {
|
||||
private final Query query;
|
||||
|
||||
/**
|
||||
* Create new a {@link QueryContext} instance from the given {@literal query} (can be eihter a {@link Query} or a
|
||||
* Create new a {@link QueryContext} instance from the given {@literal query} (can be either a {@link Query} or a
|
||||
* plain {@link Document}.
|
||||
*
|
||||
* @param query can be {@literal null}.
|
||||
@@ -305,7 +305,7 @@ class QueryOperations {
|
||||
mappingContext.getRequiredPersistentEntity(targetType));
|
||||
}
|
||||
|
||||
if (entity != null && entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) {
|
||||
if (entity.hasTextScoreProperty() && !query.getQueryObject().containsKey("$text")) {
|
||||
mappedFields.remove(entity.getTextScoreProperty().getFieldName());
|
||||
}
|
||||
|
||||
@@ -793,19 +793,19 @@ class QueryOperations {
|
||||
}
|
||||
|
||||
/**
|
||||
* A context class that encapsulates common tasks required when running {@literal aggregations}.
|
||||
* A value object that encapsulates common tasks required when running {@literal aggregations}.
|
||||
*
|
||||
* @since 3.2
|
||||
*/
|
||||
class AggregateContext {
|
||||
class AggregationDefinition {
|
||||
|
||||
private Aggregation aggregation;
|
||||
private Lazy<AggregationOperationContext> aggregationOperationContext;
|
||||
private Lazy<List<Document>> pipeline;
|
||||
private @Nullable Class<?> inputType;
|
||||
private final Aggregation aggregation;
|
||||
private final Lazy<AggregationOperationContext> aggregationOperationContext;
|
||||
private final Lazy<List<Document>> pipeline;
|
||||
private final @Nullable Class<?> inputType;
|
||||
|
||||
/**
|
||||
* Creates new instance of {@link AggregateContext} extracting the input type from either the
|
||||
* Creates new instance of {@link AggregationDefinition} extracting the input type from either the
|
||||
* {@link org.springframework.data.mongodb.core.aggregation.Aggregation} in case of a {@link TypedAggregation} or
|
||||
* the given {@literal aggregationOperationContext} if present. <br />
|
||||
* Creates a new {@link AggregationOperationContext} if none given, based on the {@link Aggregation} input type and
|
||||
@@ -815,21 +815,25 @@ class QueryOperations {
|
||||
* @param aggregation the source aggregation.
|
||||
* @param aggregationOperationContext can be {@literal null}.
|
||||
*/
|
||||
AggregateContext(Aggregation aggregation, @Nullable AggregationOperationContext aggregationOperationContext) {
|
||||
AggregationDefinition(Aggregation aggregation, @Nullable AggregationOperationContext aggregationOperationContext) {
|
||||
|
||||
this.aggregation = aggregation;
|
||||
|
||||
if (aggregation instanceof TypedAggregation) {
|
||||
this.inputType = ((TypedAggregation) aggregation).getInputType();
|
||||
this.inputType = ((TypedAggregation<?>) aggregation).getInputType();
|
||||
} else if (aggregationOperationContext instanceof TypeBasedAggregationOperationContext) {
|
||||
this.inputType = ((TypeBasedAggregationOperationContext) aggregationOperationContext).getType();
|
||||
} else {
|
||||
this.inputType = null;
|
||||
}
|
||||
|
||||
this.aggregationOperationContext = Lazy.of(() -> aggregationOperationContext != null ? aggregationOperationContext
|
||||
: aggregationUtil.createAggregationContext(aggregation, getInputType()));
|
||||
this.pipeline = Lazy.of(() -> aggregationUtil.createPipeline(this.aggregation, getAggregationOperationContext()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new instance of {@link AggregateContext} extracting the input type from either the
|
||||
* Creates new instance of {@link AggregationDefinition} extracting the input type from either the
|
||||
* {@link org.springframework.data.mongodb.core.aggregation.Aggregation} in case of a {@link TypedAggregation} or
|
||||
* the given {@literal aggregationOperationContext} if present. <br />
|
||||
* Creates a new {@link AggregationOperationContext} based on the {@link Aggregation} input type and the desired
|
||||
@@ -839,12 +843,12 @@ class QueryOperations {
|
||||
* @param aggregation the source aggregation.
|
||||
* @param inputType can be {@literal null}.
|
||||
*/
|
||||
AggregateContext(Aggregation aggregation, @Nullable Class<?> inputType) {
|
||||
AggregationDefinition(Aggregation aggregation, @Nullable Class<?> inputType) {
|
||||
|
||||
this.aggregation = aggregation;
|
||||
|
||||
if (aggregation instanceof TypedAggregation) {
|
||||
this.inputType = ((TypedAggregation) aggregation).getInputType();
|
||||
this.inputType = ((TypedAggregation<?>) aggregation).getInputType();
|
||||
} else {
|
||||
this.inputType = inputType;
|
||||
}
|
||||
@@ -887,9 +891,5 @@ class QueryOperations {
|
||||
Class<?> getInputType() {
|
||||
return inputType;
|
||||
}
|
||||
|
||||
Document getAggregationCommand(String collectionName) {
|
||||
return aggregationUtil.createCommand(collectionName, aggregation, getAggregationOperationContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ package org.springframework.data.mongodb.core;
|
||||
|
||||
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
|
||||
|
||||
import org.springframework.data.mongodb.core.QueryOperations.AggregateContext;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition;
|
||||
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
@@ -986,7 +986,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
|
||||
AggregationOptions options = aggregation.getOptions();
|
||||
Assert.isTrue(!options.isExplain(), "Cannot use explain option with streaming!");
|
||||
|
||||
AggregateContext ctx = queryOperations.createAggregationContext(aggregation, inputType);
|
||||
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, inputType);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName);
|
||||
|
||||
@@ -487,7 +487,7 @@ public class AggregationOptions {
|
||||
|
||||
/**
|
||||
* Apply a strict domain type mapping considering {@link org.springframework.data.mongodb.core.mapping.Field}
|
||||
* annotations throwing errors for non existing, but referenced fields.
|
||||
* annotations throwing errors for non-existent, but referenced fields.
|
||||
*
|
||||
* @return this.
|
||||
* @since 3.2
|
||||
@@ -512,7 +512,7 @@ public class AggregationOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply no domain type mapping at all taking the pipeline as is.
|
||||
* Apply no domain type mapping at all taking the pipeline as-is.
|
||||
*
|
||||
* @return this.
|
||||
* @since 3.2
|
||||
@@ -568,15 +568,17 @@ public class AggregationOptions {
|
||||
public enum DomainTypeMapping {
|
||||
|
||||
/**
|
||||
* Mapping throws errors for non existing, but referenced fields.
|
||||
* Mapping throws errors for non-existent, but referenced fields.
|
||||
*/
|
||||
STRICT,
|
||||
|
||||
/**
|
||||
* Fields that do not exist in the model are treated as is.
|
||||
* Fields that do not exist in the model are treated as-is.
|
||||
*/
|
||||
RELAXED,
|
||||
|
||||
/**
|
||||
* Do not attempt to map fields against the model and treat the entire pipeline as is.
|
||||
* Do not attempt to map fields against the model and treat the entire pipeline as-is.
|
||||
*/
|
||||
NONE
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.AggregateContext;
|
||||
import org.springframework.data.mongodb.core.QueryOperations.AggregationDefinition;
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
|
||||
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
|
||||
@@ -35,8 +35,9 @@ import org.springframework.data.mongodb.core.convert.UpdateMapper;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link QueryOperations}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 2021/01
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class QueryOperationsUnitTests {
|
||||
@@ -66,7 +67,7 @@ class QueryOperationsUnitTests {
|
||||
void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenNoInputTypeProvided() {
|
||||
|
||||
Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name"));
|
||||
AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class<?>) null);
|
||||
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class<?>) null);
|
||||
|
||||
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
|
||||
}
|
||||
@@ -75,7 +76,7 @@ class QueryOperationsUnitTests {
|
||||
void createAggregationContextUsesRelaxedOneForTypedAggregationsWhenNoInputTypeProvided() {
|
||||
|
||||
Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name"));
|
||||
AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class<?>) null);
|
||||
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class<?>) null);
|
||||
|
||||
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
|
||||
}
|
||||
@@ -84,7 +85,7 @@ class QueryOperationsUnitTests {
|
||||
void createAggregationContextUsesRelaxedOneForUntypedAggregationsWhenInputTypeProvided() {
|
||||
|
||||
Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name"));
|
||||
AggregateContext ctx = queryOperations.createAggregationContext(aggregation, Person.class);
|
||||
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, Person.class);
|
||||
|
||||
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(RelaxedTypeBasedAggregationOperationContext.class);
|
||||
}
|
||||
@@ -93,7 +94,7 @@ class QueryOperationsUnitTests {
|
||||
void createAggregationContextUsesDefaultIfNoMappingDesired() {
|
||||
|
||||
Aggregation aggregation = Aggregation.newAggregation(Aggregation.project("name")).withOptions(NO_MAPPING);
|
||||
AggregateContext ctx = queryOperations.createAggregationContext(aggregation, Person.class);
|
||||
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, Person.class);
|
||||
|
||||
assertThat(ctx.getAggregationOperationContext()).isEqualTo(Aggregation.DEFAULT_CONTEXT);
|
||||
}
|
||||
@@ -103,7 +104,7 @@ class QueryOperationsUnitTests {
|
||||
|
||||
Aggregation aggregation = Aggregation.newAggregation(Person.class, Aggregation.project("name"))
|
||||
.withOptions(STRICT_MAPPING);
|
||||
AggregateContext ctx = queryOperations.createAggregationContext(aggregation, (Class<?>) null);
|
||||
AggregationDefinition ctx = queryOperations.createAggregation(aggregation, (Class<?>) null);
|
||||
|
||||
assertThat(ctx.getAggregationOperationContext()).isInstanceOf(TypeBasedAggregationOperationContext.class);
|
||||
}
|
||||
|
||||
@@ -2502,7 +2502,7 @@ For further information, see the full https://docs.mongodb.org/manual/aggregatio
|
||||
[[mongo.aggregation.basic-concepts]]
|
||||
=== Basic Concepts
|
||||
|
||||
The Aggregation Framework support in Spring Data MongoDB is based on the following key abstractions: `Aggregation`, `AggregationOperation`, and `AggregationResults`.
|
||||
The Aggregation Framework support in Spring Data MongoDB is based on the following key abstractions: `Aggregation`, `AggregationDefinition`, and `AggregationResults`.
|
||||
|
||||
* `Aggregation`
|
||||
+
|
||||
@@ -2517,12 +2517,12 @@ A `TypedAggregation`, just like an `Aggregation`, holds the instructions of the
|
||||
At runtime, field references get checked against the given input type, considering potential `@Field` annotations.
|
||||
[NOTE]
|
||||
====
|
||||
Changed in 3.2 referencing nonexistent properties does no longer raise errors. To restore the previous behaviour use the `strictMapping` option of `AggregationOptions`.
|
||||
Changed in 3.2 referencing none-xistent properties does no longer raise errors. To restore the previous behaviour use the `strictMapping` option of `AggregationOptions`.
|
||||
====
|
||||
+
|
||||
* `AggregationOperation`
|
||||
* `AggregationDefinition`
|
||||
+
|
||||
An `AggregationOperation` represents a MongoDB aggregation pipeline operation and describes the processing that should be performed in this aggregation step. Although you could manually create an `AggregationOperation`, we recommend using the static factory methods provided by the `Aggregate` class to construct an `AggregateOperation`.
|
||||
An `AggregationDefinition` represents a MongoDB aggregation pipeline operation and describes the processing that should be performed in this aggregation step. Although you could manually create an `AggregationDefinition`, we recommend using the static factory methods provided by the `Aggregate` class to construct an `AggregateOperation`.
|
||||
+
|
||||
* `AggregationResults`
|
||||
+
|
||||
|
||||
Reference in New Issue
Block a user