diff --git a/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java b/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java index 16a92b6b37..871950c8bc 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java @@ -70,6 +70,17 @@ } } """), + @ExampleObject( + name = "countDocuments", + summary = "`countDocuments` command", + value = + """ + { + "countDocuments": { + "filter": {"location": "London", "race.competitors" : {"$eq" : 100}} + } + } + """), @ExampleObject( name = "find", summary = "`find` command", @@ -209,6 +220,17 @@ } } """), + @ExampleObject( + name = "resultCount", + summary = "countDocuments command result", + value = + """ + { + "status": { + "counted_documents": 2 + } + } + """), @ExampleObject( name = "resultRead", summary = "Read command result", diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java index b4e1ff232c..497524186d 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/Command.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.stargate.sgv2.jsonapi.api.model.command.impl.CountDocumentsCommands; import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateCollectionCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.CreateNamespaceCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand; @@ -34,6 +35,7 @@ include = JsonTypeInfo.As.WRAPPER_OBJECT, property = "commandName") @JsonSubTypes({ + @JsonSubTypes.Type(value = CountDocumentsCommands.class), @JsonSubTypes.Type(value = CreateNamespaceCommand.class), @JsonSubTypes.Type(value = CreateCollectionCommand.class), @JsonSubTypes.Type(value = DeleteOneCommand.class), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandStatus.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandStatus.java index a47388b99a..301e28de36 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandStatus.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/CommandStatus.java @@ -2,13 +2,21 @@ import com.fasterxml.jackson.annotation.JsonProperty; +/** Enum with it's json property name which is returned in api response inside status */ public enum CommandStatus { + /** The element has the count of document */ + @JsonProperty("counted_documents") + COUNTED_DOCUMENT, + /** The element has the list of deleted ids */ @JsonProperty("deletedIds") DELETED_IDS, + /** The element has the list of inserted ids */ @JsonProperty("insertedIds") INSERTED_IDS, + /** The element has value 1 if collection is created */ @JsonProperty("ok") OK, + /** The element has the list of updated ids */ @JsonProperty("updatedIds") UPDATED_IDS; } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CountDocumentsCommands.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CountDocumentsCommands.java new file mode 100644 index 0000000000..2970199963 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/CountDocumentsCommands.java @@ -0,0 +1,21 @@ +package io.stargate.sgv2.jsonapi.api.model.command.impl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.stargate.sgv2.jsonapi.api.model.command.Filterable; +import io.stargate.sgv2.jsonapi.api.model.command.ReadCommand; +import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterClause; +import javax.annotation.Nullable; +import javax.validation.Valid; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema( + description = + "Command that returns count of documents in a collection based on the collection.") +@JsonTypeName("countDocuments") +public record CountDocumentsCommands( + @Valid @JsonProperty("filter") FilterClause filterClause, @Valid @Nullable Options options) + implements ReadCommand, Filterable { + + public record Options() {} +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java index 0b3b6ef3f7..e80bdfccc9 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/CollectionResource.java @@ -4,6 +4,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.CollectionCommand; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.api.model.command.impl.CountDocumentsCommands; import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndUpdateCommand; @@ -66,6 +67,7 @@ public CollectionResource(CommandProcessor commandProcessor) { schema = @Schema( anyOf = { + CountDocumentsCommands.class, DeleteOneCommand.class, FindOneCommand.class, FindCommand.class, @@ -75,6 +77,7 @@ public CollectionResource(CommandProcessor commandProcessor) { UpdateOneCommand.class }), examples = { + @ExampleObject(ref = "count"), @ExampleObject(ref = "deleteOne"), @ExampleObject(ref = "findOne"), @ExampleObject(ref = "find"), @@ -93,6 +96,7 @@ public CollectionResource(CommandProcessor commandProcessor) { mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CommandResult.class), examples = { + @ExampleObject(ref = "resultCount"), @ExampleObject(ref = "resultRead"), @ExampleObject(ref = "resultFindOneAndUpdate"), @ExampleObject(ref = "resultInsert"), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java index c8c3a5a979..b8cc9112ed 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCode.java @@ -26,6 +26,8 @@ public enum ErrorCode { UNSUPPORTED_FILTER_OPERATION("Unsupported filter operator"), + UNSUPPORTED_OPERATION("Unsupported operation class"), + UNSUPPORTED_UPDATE_DATA_TYPE("Unsupported update data type"), UNSUPPORTED_UPDATE_OPERATION("Unsupported update operation"), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/CountOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/CountOperation.java new file mode 100644 index 0000000000..40c55a514a --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/CountOperation.java @@ -0,0 +1,51 @@ +package io.stargate.sgv2.jsonapi.service.operation.model; + +import io.smallrye.mutiny.Uni; +import io.stargate.bridge.proto.QueryOuterClass; +import io.stargate.sgv2.api.common.cql.builder.BuiltCondition; +import io.stargate.sgv2.api.common.cql.builder.QueryBuilder; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.exception.ErrorCode; +import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; +import io.stargate.sgv2.jsonapi.service.operation.model.impl.CountOperationPage; +import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * Operation that returns count of documents based on the filter condition. Written with the + * assumption that all variables to be indexed. + */ +public record CountOperation(CommandContext commandContext, List filters) + implements ReadOperation { + + @Override + public Uni> execute(QueryExecutor queryExecutor) { + QueryOuterClass.Query query = buildSelectQuery(); + return countDocuments(queryExecutor, query) + .onItem() + .transform(docs -> new CountOperationPage(docs.count())); + } + + private QueryOuterClass.Query buildSelectQuery() { + List conditions = new ArrayList<>(filters.size()); + for (DBFilterBase filter : filters) { + conditions.add(filter.get()); + } + return new QueryBuilder() + .select() + .count("key") + .as("count") + .from(commandContext.namespace(), commandContext.collection()) + .where(conditions) + .build(); + } + + @Override + public Uni getDocuments(QueryExecutor queryExecutor) { + return Uni.createFrom().failure(new JsonApiException(ErrorCode.UNSUPPORTED_OPERATION)); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java index ea8f4578e9..4da1851045 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadOperation.java @@ -21,8 +21,8 @@ * implementation to excute and query and parse the result set as {@link FindResponse} */ public interface ReadOperation extends Operation { - static String[] documentColumns = {"key", "tx_id", "doc_json"}; - static String[] documentKeyColumns = {"key", "tx_id"}; + String[] documentColumns = {"key", "tx_id", "doc_json"}; + String[] documentKeyColumns = {"key", "tx_id"}; /** * Default implementation to query and parse the result set @@ -90,6 +90,26 @@ private String extractPagingStateFromResultSet(QueryOuterClass.ResultSet rSet) { } return null; } + /** + * Default implementation to run count query and parse the result set + * + * @param queryExecutor + * @param query + * @return + */ + default Uni countDocuments( + QueryExecutor queryExecutor, QueryOuterClass.Query query) { + return queryExecutor + .executeRead(query, Optional.empty(), 1) + .onItem() + .transform( + rSet -> { + QueryOuterClass.Row row = rSet.getRows(0); // For count there will be only one row + int count = + Values.int_(row.getValues(0)); // Count value will be the first column value + return new CountResponse(count); + }); + } /** * A operation method which can return FindResponse instead of CommandResult. This method will be @@ -100,5 +120,7 @@ private String extractPagingStateFromResultSet(QueryOuterClass.ResultSet rSet) { */ Uni getDocuments(QueryExecutor queryExecutor); - public static record FindResponse(List docs, String pagingState) {} + record FindResponse(List docs, String pagingState) {} + + record CountResponse(int count) {} } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadType.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadType.java new file mode 100644 index 0000000000..8b6cd80698 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/ReadType.java @@ -0,0 +1,19 @@ +package io.stargate.sgv2.jsonapi.service.operation.model; + +/** + * Read type specifies what data needs to be read and returned as part of the response for + * operations + */ +public enum ReadType { + /** + * Return documents and transaction id which satisfies the filter conditions as part of response + */ + DOCUMENT, + /** + * Return only document id and transaction id of documents which satisfies the filter conditions + * as part of response + */ + KEY, + /** Return only count of documents which satisfies the filter condition */ + COUNT +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CountOperationPage.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CountOperationPage.java new file mode 100644 index 0000000000..aa3640f880 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CountOperationPage.java @@ -0,0 +1,13 @@ +package io.stargate.sgv2.jsonapi.service.operation.model.impl; + +import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.api.model.command.CommandStatus; +import java.util.Map; +import java.util.function.Supplier; + +public record CountOperationPage(int count) implements Supplier { + @Override + public CommandResult get() { + return new CommandResult(Map.of(CommandStatus.COUNTED_DOCUMENT, count())); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java index 64ddf1d500..4997ad4a83 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperation.java @@ -7,25 +7,23 @@ import io.stargate.sgv2.api.common.cql.builder.QueryBuilder; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.exception.ErrorCode; +import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; -/** - * Full dynamic query generation for any of the types of filtering we can do against the the db - * table. - * - *

Create with a series of filters that are implicitly AND'd together. - */ +/** Operation that returns the documents or its key based on the filter condition. */ public record FindOperation( CommandContext commandContext, List filters, String pagingState, int limit, int pageSize, - boolean readDocument, + ReadType readType, ObjectMapper objectMapper) implements ReadOperation { @@ -38,8 +36,23 @@ public Uni> execute(QueryExecutor queryExecutor) { @Override public Uni getDocuments(QueryExecutor queryExecutor) { - QueryOuterClass.Query query = buildSelectQuery(); - return findDocument(queryExecutor, query, pagingState, pageSize, readDocument, objectMapper); + switch (readType) { + case DOCUMENT: + case KEY: + { + QueryOuterClass.Query query = buildSelectQuery(); + return findDocument( + queryExecutor, + query, + pagingState, + pageSize, + ReadType.DOCUMENT == readType, + objectMapper); + } + default: + throw new JsonApiException( + ErrorCode.UNSUPPORTED_OPERATION, "Unsupported find operation read type " + readType); + } } private QueryOuterClass.Query buildSelectQuery() { @@ -49,7 +62,7 @@ private QueryOuterClass.Query buildSelectQuery() { } return new QueryBuilder() .select() - .column(readDocument ? documentColumns : documentKeyColumns) + .column(ReadType.DOCUMENT == readType ? documentColumns : documentKeyColumns) .from(commandContext.namespace(), commandContext.collection()) .where(conditions) .limit(limit) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadOperationPage.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadOperationPage.java index 43aab8ccf9..4f30880545 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadOperationPage.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadOperationPage.java @@ -9,7 +9,6 @@ /** FindOperation response implementing the {@link CommandResult} */ public record ReadOperationPage(List docs, String pagingState) implements Supplier { - @Override public CommandResult get() { final List jsonNodes = new ArrayList<>(); diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/CountDocumentsCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/CountDocumentsCommandResolver.java new file mode 100644 index 0000000000..65f84e6428 --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/CountDocumentsCommandResolver.java @@ -0,0 +1,36 @@ +package io.stargate.sgv2.jsonapi.service.resolver.model.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.impl.CountDocumentsCommands; +import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; +import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; +import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +/** Resolves the {@link CountDocumentsCommands } */ +@ApplicationScoped +public class CountDocumentsCommandResolver extends FilterableResolver + implements CommandResolver { + @Inject + public CountDocumentsCommandResolver(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + public Class getCommandClass() { + return CountDocumentsCommands.class; + } + + @Override + public Operation resolveCommand(CommandContext ctx, CountDocumentsCommands command) { + return resolve(ctx, command); + } + + @Override + protected FilteringOptions getFilteringOption(CountDocumentsCommands command) { + return new FilteringOptions(Integer.MAX_VALUE, null, 1, ReadType.COUNT); + } +} diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java index f5cc640ddb..cd835da044 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolver.java @@ -5,6 +5,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DeleteOperation; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; @@ -21,7 +22,7 @@ public class DeleteOneCommandResolver extends FilterableResolver getCommandClass() { @Override protected FilteringOptions getFilteringOption(DeleteOneCommand command) { - return new FilteringOptions(1, null, 1); + return new FilteringOptions(1, null, 1, ReadType.KEY); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java index 6bfe495154..beba8e1e2e 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolver.java @@ -6,6 +6,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneCommand; import io.stargate.sgv2.jsonapi.service.bridge.config.DocumentConfig; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; import javax.enterprise.context.ApplicationScoped; @@ -20,14 +21,10 @@ public class FindCommandResolver extends FilterableResolver @Inject public FindCommandResolver(DocumentConfig documentConfig, ObjectMapper objectMapper) { - super(objectMapper, false, true); + super(objectMapper); this.documentConfig = documentConfig; } - public FindCommandResolver() { - this(null, null); - } - @Override public Class getCommandClass() { return FindCommand.class; @@ -49,6 +46,6 @@ protected FilteringOptions getFilteringOption(FindCommand command) { ? command.options().pageSize() : documentConfig.defaultPageSize(); String pagingState = command.options() != null ? command.options().pagingState() : null; - return new FilteringOptions(limit, pagingState, pageSize); + return new FilteringOptions(limit, pagingState, pageSize, ReadType.DOCUMENT); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java index 0bf3618b01..9f1e6af802 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateCommandResolver.java @@ -5,6 +5,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndUpdateCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; @@ -21,7 +22,7 @@ public class FindOneAndUpdateCommandResolver extends FilterableResolver @Inject public FindOneCommandResolver(ObjectMapper objectMapper) { - super(objectMapper, true, true); + super(objectMapper); } @Override @@ -31,6 +32,6 @@ public Operation resolveCommand(CommandContext ctx, FindOneCommand command) { @Override protected FilteringOptions getFilteringOption(FindOneCommand command) { - return new FilteringOptions(1, null, 1); + return new FilteringOptions(1, null, 1, ReadType.DOCUMENT); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java index a985802060..51711f3900 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneCommandResolver.java @@ -5,6 +5,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateOneCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; import io.stargate.sgv2.jsonapi.service.resolver.model.impl.matcher.FilterableResolver; @@ -21,7 +22,7 @@ public class UpdateOneCommandResolver extends FilterableResolver { private static final Object ARRAY_EQUALS = new Object(); private static final Object SUB_DOC_EQUALS = new Object(); - private final boolean findOne; - private final boolean readDocument; - private final ObjectMapper objectMapper; protected FilterableResolver() { - this(null, false, false); + this(null); } @Inject - public FilterableResolver(ObjectMapper objectMapper, boolean findOne, boolean readDocument) { + public FilterableResolver(ObjectMapper objectMapper) { this.objectMapper = objectMapper; - this.findOne = findOne; - this.readDocument = readDocument; matchRules.addMatchRule(this::findNoFilter, FilterMatcher.MatchStrategy.EMPTY); matchRules @@ -98,7 +95,7 @@ protected ReadOperation resolve(CommandContext commandContext, T command) { return matchRules.apply(commandContext, command); } - public record FilteringOptions(int limit, String pagingState, int pageSize) {} + public record FilteringOptions(int limit, String pagingState, int pageSize, ReadType readType) {} protected abstract FilteringOptions getFilteringOption(T command); @@ -115,26 +112,34 @@ private ReadOperation findById(CommandContext commandContext, CaptureGroups c DBFilterBase.IDFilter.Operator.EQ, expression.value()))); } FilteringOptions filteringOptions = getFilteringOption(captures.command()); - return new FindOperation( - commandContext, - filters, - filteringOptions.pagingState(), - filteringOptions.limit(), - filteringOptions.pageSize(), - readDocument, - objectMapper); + if (filteringOptions.readType() == ReadType.COUNT) { + return new CountOperation(commandContext, filters); + } else { + return new FindOperation( + commandContext, + filters, + filteringOptions.pagingState(), + filteringOptions.limit(), + filteringOptions.pageSize(), + filteringOptions.readType(), + objectMapper); + } } private ReadOperation findNoFilter(CommandContext commandContext, CaptureGroups captures) { FilteringOptions filteringOptions = getFilteringOption(captures.command()); - return new FindOperation( - commandContext, - List.of(), - filteringOptions.pagingState(), - filteringOptions.limit(), - filteringOptions.pageSize(), - readDocument, - objectMapper); + if (filteringOptions.readType() == ReadType.COUNT) { + return new CountOperation(commandContext, List.of()); + } else { + return new FindOperation( + commandContext, + List.of(), + filteringOptions.pagingState(), + filteringOptions.limit(), + filteringOptions.pageSize(), + filteringOptions.readType(), + objectMapper); + } } private ReadOperation findDynamic(CommandContext commandContext, CaptureGroups captures) { @@ -250,13 +255,17 @@ private ReadOperation findDynamic(CommandContext commandContext, CaptureGroups { + FilterClause filterClause = countCommand.filterClause(); + assertThat(filterClause).isNotNull(); + }); + } + } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CountIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CountIntegrationTest.java new file mode 100644 index 0000000000..91b8624cde --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/CountIntegrationTest.java @@ -0,0 +1,647 @@ +package io.stargate.sgv2.jsonapi.api.v1; + +import static io.restassured.RestAssured.given; +import static io.stargate.sgv2.common.IntegrationTestUtils.getAuthToken; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; + +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; +import io.stargate.sgv2.api.common.config.constants.HttpConstants; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; + +@QuarkusIntegrationTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class CountIntegrationTest extends CollectionResourceBaseIntegrationTest { + @Nested + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class Count { + @Test + @Order(1) + public void setUp() { + String json = + """ + { + "insertOne": { + "document": { + "_id": "doc1", + "username": "user1", + "active_user" : true + } + } + } + """; + + insert(json); + json = + """ + { + "insertOne": { + "document": { + "_id": "doc2", + "username": "user2", + "subdoc" : { + "id" : "abc" + }, + "array" : [ + "value1" + ] + } + } + } + """; + + insert(json); + json = + """ + { + "insertOne": { + "document": { + "_id": "doc3", + "username": "user3", + "tags" : ["tag1", "tag2", "tag1234567890123456789012345", null, 1, true], + "nestedArray" : [["tag1", "tag2"], ["tag1234567890123456789012345", null]] + } + } + } + """; + + insert(json); + json = + """ + { + "insertOne": { + "document": { + "_id": "doc4", + "indexedObject" : { "0": "value_0", "1": "value_1" } + } + } + } + """; + + insert(json); + json = + """ + { + "insertOne": { + "document": { + "_id": "doc5", + "username": "user5", + "sub_doc" : { "a": 5, "b": { "c": "v1", "d": false } } + } + } + } + """; + + insert(json); + } + + private void insert(String json) { + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200); + } + + @Test + @Order(2) + public void countNoFilter() { + String json = + """ + { + "countDocuments": { + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(5)); + } + + @Test + @Order(2) + public void countByColumn() { + String json = + """ + { + "countDocuments": { + "filter" : {"username" : "user1"} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithEqComparisonOperator() { + String json = + """ + { + "countDocuments": { + "filter" : {"username" : {"$eq" : "user1"}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithEqSubDoc() { + String json = + """ + { + "countDocuments": { + "filter" : {"subdoc.id" : {"$eq" : "abc"}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithEqSubDocWithIndex() { + String json = + """ + { + "countDocuments": { + "filter" : {"indexedObject.1" : {"$eq" : "value_1"}} + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithEqArrayElement() { + String json = + """ + { + "countDocuments": { + "filter" : {"array.0" : {"$eq" : "value1"}} + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithExistFalseOperator() { + String json = + """ + { + "countDocuments": { + "filter" : {"active_user" : {"$exists" : false}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("errors[0].message", is("$exists is supported only with true option")); + } + + @Test + @Order(2) + public void countWithExistOperator() { + String json = + """ + { + "countDocuments": { + "filter" : {"active_user" : {"$exists" : true}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithAllOperator() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$all" : ["tag1", "tag2"]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithAllOperatorLongerString() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$all" : ["tag1", "tag1234567890123456789012345"]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithAllOperatorMixedAFormatArray() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$all" : ["tag1", 1, true, null]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithAllOperatorNoMatch() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$all" : ["tag1", 2, true, null]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(0)); + } + + @Test + @Order(2) + public void countWithEqSubdocumentShortcut() { + String json = + """ + { + "countDocuments": { + "filter" : {"sub_doc" : { "a": 5, "b": { "c": "v1", "d": false } } } + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithEqSubdocument() { + String json = + """ + { + "countDocuments": { + "filter" : {"sub_doc" : { "$eq" : { "a": 5, "b": { "c": "v1", "d": false } } } } + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Order(2) + public void countWithEqSubdocumentOrderChangeNoMatch() { + String json = + """ + { + "countDocuments": { + "filter" : {"sub_doc" : { "$eq" : { "a": 5, "b": { "d": false, "c": "v1" } } } } + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(0)); + } + + @Test + @Order(2) + public void countWithEqSubdocumentNoMatch() { + String json = + """ + { + "countDocuments": { + "filter" : {"sub_doc" : { "$eq" : { "a": 5, "b": { "c": "v1", "d": true } } } } + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(0)); + } + + @Test + @Order(2) + public void countWithSizeOperator() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$size" : 6}} + } + } + """; + + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithSizeOperatorNoMatch() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$size" : 1}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(0)); + } + + @Test + @Order(2) + public void countWithEqOperatorArray() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$eq" : ["tag1", "tag2", "tag1234567890123456789012345", null, 1, true]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Order(2) + public void countWithEqOperatorNestedArray() { + String json = + """ + { + "countDocuments": { + "filter" : {"nestedArray" : {"$eq" : [["tag1", "tag2"], ["tag1234567890123456789012345", null]]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + + @Test + @Order(2) + public void countWithEqOperatorArrayNoMatch() { + String json = + """ + { + "countDocuments": { + "filter" : {"tags" : {"$eq" : ["tag1", "tag2", "tag1234567890123456789012345", null, 1]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(0)); + } + + @Order(2) + public void countWithEqOperatorNestedArrayNoMatch() { + String json = + """ + { + "countDocuments": { + "filter" : {"nestedArray" : {"$eq" : [["tag1", "tag2"], ["tag1234567890123456789012345", null], ["abc"]]}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(0)); + } + + @Test + @Order(2) + public void countWithNEComparisonOperator() { + String json = + """ + { + "countDocuments": { + "filter" : {"username" : {"$ne" : "user1"}} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("errors[1].message", startsWith("Unsupported filter operator $ne")); + } + + @Test + @Order(2) + public void countByBooleanColumn() { + String json = + """ + { + "countDocuments": { + "filter" : {"active_user" : true} + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200) + .body("status.counted_documents", is(1)); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindIntegrationTest.java index c8675a6704..813c4c55e4 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindIntegrationTest.java @@ -41,15 +41,7 @@ public void setUp() { } """; - given() - .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) - .contentType(ContentType.JSON) - .body(json) - .when() - .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) - .then() - .statusCode(200); - + insert(json); json = """ { @@ -68,14 +60,7 @@ public void setUp() { } """; - given() - .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) - .contentType(ContentType.JSON) - .body(json) - .when() - .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) - .then() - .statusCode(200); + insert(json); json = """ @@ -91,14 +76,7 @@ public void setUp() { } """; - given() - .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) - .contentType(ContentType.JSON) - .body(json) - .when() - .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) - .then() - .statusCode(200); + insert(json); json = """ @@ -112,14 +90,7 @@ public void setUp() { } """; - given() - .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) - .contentType(ContentType.JSON) - .body(json) - .when() - .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) - .then() - .statusCode(200); + insert(json); json = """ @@ -134,6 +105,10 @@ public void setUp() { } """; + insert(json); + } + + private void insert(String json) { given() .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) .contentType(ContentType.JSON) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CountOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CountOperationTest.java new file mode 100644 index 0000000000..7135234e2b --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/CountOperationTest.java @@ -0,0 +1,129 @@ +package io.stargate.sgv2.jsonapi.service.operation.model.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.bridge.grpc.TypeSpecs; +import io.stargate.bridge.grpc.Values; +import io.stargate.bridge.proto.QueryOuterClass; +import io.stargate.sgv2.common.bridge.AbstractValidatingStargateBridgeTest; +import io.stargate.sgv2.common.bridge.ValidatingStargateBridge; +import io.stargate.sgv2.common.testprofiles.NoGlobalResourcesTestProfile; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; +import io.stargate.sgv2.jsonapi.api.model.command.CommandStatus; +import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; +import io.stargate.sgv2.jsonapi.service.operation.model.CountOperation; +import java.util.List; +import java.util.function.Supplier; +import javax.inject.Inject; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +public class CountOperationTest extends AbstractValidatingStargateBridgeTest { + private static final String KEYSPACE_NAME = RandomStringUtils.randomAlphanumeric(16); + private static final String COLLECTION_NAME = RandomStringUtils.randomAlphanumeric(16); + private CommandContext commandContext = new CommandContext(KEYSPACE_NAME, COLLECTION_NAME); + + @Inject QueryExecutor queryExecutor; + @Inject ObjectMapper objectMapper; + + @Nested + class CountOperationsTest { + @Test + public void countWithNoFilter() throws Exception { + String collectionReadCql = + "SELECT COUNT(key) AS count FROM \"%s\".\"%s\"".formatted(KEYSPACE_NAME, COLLECTION_NAME); + + ValidatingStargateBridge.QueryAssert candidatesAssert = + withQuery(collectionReadCql) + .withPageSize(1) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("count") + .setType(TypeSpecs.INT) + .build())) + .returning(List.of(List.of(Values.of(5)))); + CountOperation countOperation = new CountOperation(commandContext, List.of()); + final Supplier execute = + countOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); + CommandResult result = execute.get(); + assertThat(result) + .satisfies( + commandResult -> { + assertThat(result.status().get(CommandStatus.COUNTED_DOCUMENT)).isNotNull(); + assertThat(result.status().get(CommandStatus.COUNTED_DOCUMENT)).isEqualTo(5); + }); + } + + @Test + public void countWithDynamic() throws Exception { + String collectionReadCql = + "SELECT COUNT(key) AS count FROM \"%s\".\"%s\" WHERE query_text_values[?] = ?" + .formatted(KEYSPACE_NAME, COLLECTION_NAME); + ValidatingStargateBridge.QueryAssert candidatesAssert = + withQuery(collectionReadCql, Values.of("username"), Values.of("user1")) + .withPageSize(1) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("count") + .setType(TypeSpecs.INT) + .build())) + .returning(List.of(List.of(Values.of(2)))); + CountOperation countOperation = + new CountOperation( + commandContext, + List.of( + new DBFilterBase.TextFilter( + "username", DBFilterBase.MapFilterBase.Operator.EQ, "user1"))); + final Supplier execute = + countOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); + CommandResult result = execute.get(); + assertThat(result) + .satisfies( + commandResult -> { + assertThat(result.status().get(CommandStatus.COUNTED_DOCUMENT)).isNotNull(); + assertThat(result.status().get(CommandStatus.COUNTED_DOCUMENT)).isEqualTo(2); + }); + } + + @Test + public void countWithDynamicNoMatch() throws Exception { + String collectionReadCql = + "SELECT COUNT(key) AS count FROM \"%s\".\"%s\" WHERE query_text_values[?] = ?" + .formatted(KEYSPACE_NAME, COLLECTION_NAME); + ValidatingStargateBridge.QueryAssert candidatesAssert = + withQuery(collectionReadCql, Values.of("username"), Values.of("user_all")) + .withPageSize(1) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("count") + .setType(TypeSpecs.INT) + .build())) + .returning(List.of(List.of(Values.of(0)))); + CountOperation countOperation = + new CountOperation( + commandContext, + List.of( + new DBFilterBase.TextFilter( + "username", DBFilterBase.MapFilterBase.Operator.EQ, "user_all"))); + final Supplier execute = + countOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); + CommandResult result = execute.get(); + assertThat(result) + .satisfies( + commandResult -> { + assertThat(result.status().get(CommandStatus.COUNTED_DOCUMENT)).isNotNull(); + assertThat(result.status().get(CommandStatus.COUNTED_DOCUMENT)).isEqualTo(0); + }); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java index 7900b4ae73..bba3bf96cd 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/DeleteOperationTest.java @@ -16,6 +16,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandStatus; import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.bridge.serializer.CustomValueSerializers; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.List; import java.util.UUID; @@ -87,7 +88,7 @@ public void deleteWithId() throws Exception { null, 1, 1, - false, + ReadType.KEY, objectMapper); DeleteOperation operation = new DeleteOperation(commandContext, findOperation); @@ -138,7 +139,7 @@ public void deleteWithIdNoData() throws Exception { null, 1, 1, - false, + ReadType.KEY, objectMapper); DeleteOperation operation = new DeleteOperation(commandContext, findOperation); @@ -200,7 +201,7 @@ public void deleteWithDynamic() throws Exception { null, 1, 1, - false, + ReadType.KEY, objectMapper); DeleteOperation operation = new DeleteOperation(commandContext, findOperation); @@ -257,7 +258,7 @@ public void deleteWithNoResult() throws Exception { null, 1, 1, - false, + ReadType.KEY, objectMapper); DeleteOperation operation = new DeleteOperation(commandContext, findOperation); final Supplier execute = diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java index b6564079ea..51f31bec72 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/FindOperationTest.java @@ -15,6 +15,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; import io.stargate.sgv2.jsonapi.service.bridge.executor.QueryExecutor; import io.stargate.sgv2.jsonapi.service.bridge.serializer.CustomValueSerializers; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.shredding.model.DocValueHasher; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.List; @@ -91,7 +92,7 @@ public void findAll() throws Exception { Values.of(UUID.randomUUID()), Values.of(doc2)))); FindOperation findOperation = - new FindOperation(commandContext, List.of(), null, 2, 2, true, objectMapper); + new FindOperation(commandContext, List.of(), null, 2, 2, ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); CommandResult result = execute.get(); @@ -152,7 +153,7 @@ public void findWithId() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -200,7 +201,7 @@ public void findWithIdNoData() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -259,7 +260,7 @@ public void findWithDynamic() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -319,7 +320,7 @@ public void findWithBooleanFilter() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -377,7 +378,7 @@ public void findWithExistsFilter() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -438,7 +439,7 @@ public void findWithAllFilter() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -498,7 +499,7 @@ public void findWithSizeFilter() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -560,7 +561,7 @@ public void findWithArrayEqualFilter() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -622,7 +623,7 @@ public void findWithSubDocEqualFilter() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); @@ -674,7 +675,7 @@ public void findWithNoResult() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); final Supplier execute = findOperation.execute(queryExecutor).subscribeAsCompletionStage().get(); diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/CountCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/CountCommandResolverTest.java new file mode 100644 index 0000000000..3f70bb57ba --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/CountCommandResolverTest.java @@ -0,0 +1,69 @@ +package io.stargate.sgv2.jsonapi.service.resolver.model.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.stargate.sgv2.common.testprofiles.NoGlobalResourcesTestProfile; +import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; +import io.stargate.sgv2.jsonapi.api.model.command.impl.CountDocumentsCommands; +import io.stargate.sgv2.jsonapi.service.operation.model.CountOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; +import java.util.List; +import javax.inject.Inject; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +public class CountCommandResolverTest { + @Inject ObjectMapper objectMapper; + @Inject CountDocumentsCommandResolver countCommandResolver; + + @Test + public void noFilterCondition() throws Exception { + String json = + """ + { + "countDocuments": { + + } + } + """; + + CountDocumentsCommands countCommand = + objectMapper.readValue(json, CountDocumentsCommands.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = countCommandResolver.resolveCommand(commandContext, countCommand); + CountOperation expected = new CountOperation(commandContext, List.of()); + assertThat(operation) + .isInstanceOf(CountOperation.class) + .satisfies(op -> assertThat(op).isEqualTo(expected)); + } + + @Test + public void dynamicFilterCondition() throws Exception { + String json = + """ + { + "countDocuments": { + "filter" : {"col" : "val"} + } + } + """; + + CountDocumentsCommands countCommand = + objectMapper.readValue(json, CountDocumentsCommands.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = countCommandResolver.resolveCommand(commandContext, countCommand); + CountOperation expected = + new CountOperation( + commandContext, + List.of( + new DBFilterBase.TextFilter("col", DBFilterBase.MapFilterBase.Operator.EQ, "val"))); + assertThat(operation) + .isInstanceOf(CountOperation.class) + .satisfies(op -> assertThat(op).isEqualTo(expected)); + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolverTest.java index 0c2bfc21bd..978072f852 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/DeleteOneCommandResolverTest.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.model.command.impl.DeleteOneCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DeleteOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; @@ -51,7 +52,7 @@ public void idFilterCondition() throws Exception { null, 1, 1, - false, + ReadType.KEY, objectMapper); DeleteOperation expected = new DeleteOperation(commandContext, findOperation); assertThat(operation) @@ -77,7 +78,7 @@ public void noFilterCondition() throws Exception { final Operation operation = deleteOneCommandResolver.resolveCommand(commandContext, deleteOneCommand); FindOperation findOperation = - new FindOperation(commandContext, List.of(), null, 1, 1, false, objectMapper); + new FindOperation(commandContext, List.of(), null, 1, 1, ReadType.KEY, objectMapper); DeleteOperation expected = new DeleteOperation(commandContext, findOperation); assertThat(operation) .isInstanceOf(DeleteOperation.class) @@ -111,7 +112,7 @@ public void dynamicFilterCondition() throws Exception { null, 1, 1, - false, + ReadType.KEY, objectMapper); DeleteOperation expected = new DeleteOperation(commandContext, findOperation); assertThat(operation) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java index ac0e37d9aa..54d234c774 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindCommandResolverTest.java @@ -10,6 +10,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.FindCommand; import io.stargate.sgv2.jsonapi.service.bridge.config.DocumentConfig; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; @@ -51,7 +52,7 @@ public void idFilterCondition() throws Exception { null, documentConfig.maxLimit(), documentConfig.defaultPageSize(), - true, + ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) @@ -82,7 +83,7 @@ public void noFilterCondition() throws Exception { null, documentConfig.maxLimit(), documentConfig.defaultPageSize(), - true, + ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) @@ -113,7 +114,7 @@ public void noFilterConditionWithOptions() throws Exception { findCommandResolver.resolveCommand(commandContext, findOneCommand); FindOperation expected = new FindOperation( - commandContext, List.of(), "dlavjhvbavkjbna", 10, 5, true, objectMapper); + commandContext, List.of(), "dlavjhvbavkjbna", 10, 5, ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) .satisfies( @@ -146,7 +147,7 @@ public void dynamicFilterCondition() throws Exception { null, documentConfig.maxLimit(), documentConfig.defaultPageSize(), - true, + ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateResolverTest.java index a1343645dc..70190b480d 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneAndUpdateResolverTest.java @@ -11,6 +11,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneAndUpdateCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; @@ -59,7 +60,7 @@ public void idFilterConditionBsonType() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); DocumentUpdater documentUpdater = @@ -104,7 +105,7 @@ public void dynamicFilterCondition() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); DocumentUpdater documentUpdater = diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolverTest.java index 857f362113..c4c799ca50 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/FindOneCommandResolverTest.java @@ -9,6 +9,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; @@ -54,7 +55,7 @@ public void idFilterCondition() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) @@ -83,7 +84,7 @@ public void noFilterCondition() throws Exception { final Operation operation = findOneCommandResolver.resolveCommand(commandContext, findOneCommand); FindOperation expected = - new FindOperation(commandContext, List.of(), null, 1, 1, true, objectMapper); + new FindOperation(commandContext, List.of(), null, 1, 1, ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) .satisfies( @@ -120,7 +121,7 @@ public void dynamicFilterCondition() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); assertThat(operation) .isInstanceOf(FindOperation.class) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneResolverTest.java index 0d9644458d..41ea23f5a8 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneResolverTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateOneResolverTest.java @@ -11,6 +11,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateOneCommand; import io.stargate.sgv2.jsonapi.service.operation.model.Operation; import io.stargate.sgv2.jsonapi.service.operation.model.ReadOperation; +import io.stargate.sgv2.jsonapi.service.operation.model.ReadType; import io.stargate.sgv2.jsonapi.service.operation.model.impl.DBFilterBase; import io.stargate.sgv2.jsonapi.service.operation.model.impl.FindOperation; import io.stargate.sgv2.jsonapi.service.operation.model.impl.ReadAndUpdateOperation; @@ -58,7 +59,7 @@ public void idFilterConditionBsonType() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); DocumentUpdater documentUpdater = @@ -102,7 +103,7 @@ public void dynamicFilterCondition() throws Exception { null, 1, 1, - true, + ReadType.DOCUMENT, objectMapper); DocumentUpdater documentUpdater =