From 29d23f7f6eeb58cb69190a9dbf0f4cf15d7a8487 Mon Sep 17 00:00:00 2001 From: Mahesh Rajamani <99678631+maheshrajamani@users.noreply.github.com> Date: Fri, 24 Feb 2023 16:23:20 -0500 Subject: [PATCH] Update Many command implementation (#192) --- .../sgv2/jsonapi/StargateJsonApi.java | 41 +- .../jsonapi/api/model/command/Command.java | 2 + .../api/model/command/CommandStatus.java | 16 +- .../model/command/impl/UpdateManyCommand.java | 23 + .../jsonapi/api/v1/CollectionResource.java | 4 + .../service/bridge/config/DocumentConfig.java | 11 +- .../operation/model/CountOperation.java | 2 +- .../operation/model/ReadOperation.java | 2 +- .../operation/model/impl/FindOperation.java | 2 +- .../model/impl/ReadAndUpdateOperation.java | 95 +++- .../model/impl/UpdateOperationPage.java | 25 +- .../impl/FindOneAndUpdateCommandResolver.java | 2 +- .../model/impl/UpdateManyCommandResolver.java | 63 ++ .../model/impl/UpdateOneCommandResolver.java | 2 +- .../service/updater/DocumentUpdater.java | 10 +- .../api/v1/FindAndUpdateIntegrationTest.java | 61 +- .../api/v1/UpdateManyIntegrationTest.java | 342 +++++++++++ .../impl/ReadAndUpdateOperationTest.java | 536 +++++++++++++++--- .../impl/FindOneAndUpdateResolverTest.java | 6 +- .../impl/UpdateManyCommandResolverTest.java | 189 ++++++ .../model/impl/UpdateOneResolverTest.java | 4 +- .../service/updater/DocumentUpdaterTest.java | 78 +-- 22 files changed, 1350 insertions(+), 166 deletions(-) create mode 100644 src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/UpdateManyCommand.java create mode 100644 src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/api/v1/UpdateManyIntegrationTest.java create mode 100644 src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolverTest.java diff --git a/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java b/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java index 0e7da9c0c0..9e80934b15 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/StargateJsonApi.java @@ -130,6 +130,24 @@ } } """), + @ExampleObject( + name = "updateMany", + summary = "`updateMany` command", + value = + """ + { + "updateMany": { + "filter": {"location": "London"}, + "update": { + "$set": {"location": "New York"}, + "$push": {"tags": "marathon"} + }, + "options" : { + "upsert" : true + } + } + } + """), @ExampleObject( name = "deleteOne", summary = "`deleteOne` command", @@ -313,7 +331,9 @@ ], "count": 1, "status": { - "updatedIds": ["1"] + "upsertedId": "1", + "matchedCount": 0, + "modifiedCount": 1, } } } @@ -325,11 +345,28 @@ """ { "status": { - "updatedIds": ["1"] + "upsertedId": "1", + "matchedCount": 0, + "modifiedCount": 1, } } } """), + @ExampleObject( + name = "resultUpdateMany", + summary = "`updateMany` command result", + value = + """ + { + "status": { + "upsertedId": "1", + "matchedCount": 0, + "modifiedCount": 1, + "moreData" : true + } + } + } + """), @ExampleObject( name = "resultInsert", summary = "Insert 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 d399fde761..a3851125d8 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 @@ -13,6 +13,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.InsertManyCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.InsertOneCommand; +import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateManyCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateOneCommand; import io.stargate.sgv2.jsonapi.service.resolver.model.CommandResolver; @@ -48,6 +49,7 @@ @JsonSubTypes.Type(value = FindOneAndUpdateCommand.class), @JsonSubTypes.Type(value = InsertOneCommand.class), @JsonSubTypes.Type(value = InsertManyCommand.class), + @JsonSubTypes.Type(value = UpdateManyCommand.class), @JsonSubTypes.Type(value = UpdateOneCommand.class), }) public interface Command {} 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 54edd14281..0c82a0364e 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 @@ -13,6 +13,13 @@ public enum CommandStatus { /** The element has the list of inserted ids */ @JsonProperty("insertedIds") INSERTED_IDS, + /** The element has the count of document read for the update operation */ + @JsonProperty("matchedCount") + MATCHED_COUNT, + + /** The element has the count of document modified for the update operation */ + @JsonProperty("modifiedCount") + MODIFIED_COUNT, /** * The element with boolean 'true' represents if more document to be processed for updateMany and * deleteMany commands @@ -22,7 +29,10 @@ public enum CommandStatus { /** The element has value 1 if collection is created */ @JsonProperty("ok") OK, - /** The element has the list of updated ids */ - @JsonProperty("updatedIds") - UPDATED_IDS; + /** + * The element has the document id of newly inserted document part of update, when upserted option + * is 'true' and no document available in DB for matching condition + */ + @JsonProperty("upsertedId") + UPSERTED_ID; } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/UpdateManyCommand.java b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/UpdateManyCommand.java new file mode 100644 index 0000000000..40733ba44c --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/model/command/impl/UpdateManyCommand.java @@ -0,0 +1,23 @@ +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 io.stargate.sgv2.jsonapi.api.model.command.clause.update.UpdateClause; +import javax.annotation.Nullable; +import javax.validation.Valid; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +@Schema( + description = + "Command that finds documents from a collection and updates it with the values provided in the update clause.") +@JsonTypeName("updateMany") +public record UpdateManyCommand( + @Valid @JsonProperty("filter") FilterClause filterClause, + @Valid @JsonProperty("update") UpdateClause updateClause, + @Nullable Options options) + implements ReadCommand, Filterable { + public record Options(boolean upsert) {} +} 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 0a87f74319..8707f58e79 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 @@ -12,6 +12,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.impl.FindOneCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.InsertManyCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.InsertOneCommand; +import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateManyCommand; import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateOneCommand; import io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants; import io.stargate.sgv2.jsonapi.service.processor.CommandProcessor; @@ -76,6 +77,7 @@ public CollectionResource(CommandProcessor commandProcessor) { FindOneAndUpdateCommand.class, InsertOneCommand.class, InsertManyCommand.class, + UpdateManyCommand.class, UpdateOneCommand.class }), examples = { @@ -87,6 +89,7 @@ public CollectionResource(CommandProcessor commandProcessor) { @ExampleObject(ref = "findOneAndUpdate"), @ExampleObject(ref = "insertOne"), @ExampleObject(ref = "insertMany"), + @ExampleObject(ref = "updateMany"), @ExampleObject(ref = "updateOne"), })) @APIResponses( @@ -105,6 +108,7 @@ public CollectionResource(CommandProcessor commandProcessor) { @ExampleObject(ref = "resultInsert"), @ExampleObject(ref = "resultError"), @ExampleObject(ref = "resultDelete"), + @ExampleObject(ref = "resultUpdateMany"), @ExampleObject(ref = "resultUpdateOne"), }))) @POST diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/bridge/config/DocumentConfig.java b/src/main/java/io/stargate/sgv2/jsonapi/service/bridge/config/DocumentConfig.java index 1433f8439c..068e6889df 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/bridge/config/DocumentConfig.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/bridge/config/DocumentConfig.java @@ -48,11 +48,20 @@ public interface DocumentConfig { int maxLimit(); /** - * @return Defines the maximum limit of document that can be returned for a request, defaults to + * @return Defines the maximum limit of document that can be deleted for a request, defaults to * 20. */ @Max(100) @Positive @WithDefault("20") int maxDocumentDeleteCount(); + + /** + * @return Defines the maximum limit of document that can be updated for a request, defaults to + * 20. + */ + @Max(100) + @Positive + @WithDefault("20") + int maxDocumentUpdateCount(); } 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 index 86635de2f5..2d99902220 100644 --- 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 @@ -51,7 +51,7 @@ public Uni getDocuments(QueryExecutor queryExecutor, String paging } @Override - public ReadDocument getEmptyDocuments() { + public ReadDocument getNewDocument() { throw 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 4b3d4a21fd..4f704a27e4 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 @@ -126,7 +126,7 @@ default Uni countDocuments( * * @return */ - ReadDocument getEmptyDocuments(); + ReadDocument getNewDocument(); record FindResponse(List docs, String pagingState) {} 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 a4a9790db5..1a00623932 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 @@ -59,7 +59,7 @@ public Uni getDocuments(QueryExecutor queryExecutor, String paging } @Override - public ReadDocument getEmptyDocuments() { + public ReadDocument getNewDocument() { ObjectNode rootNode = objectMapper().createObjectNode(); DocumentId documentId = null; for (DBFilterBase filter : filters) { diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java index 46e8786d8d..184336413a 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperation.java @@ -16,9 +16,23 @@ import io.stargate.sgv2.jsonapi.service.shredding.model.WritableShreddedDocument; import io.stargate.sgv2.jsonapi.service.updater.DocumentUpdater; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import java.util.stream.Stream; +/** + * This operation method is used for 3 commands findOneAndUpdate, updateOne and updateMany + * + * @param commandContext + * @param readOperation + * @param documentUpdater + * @param returnDocumentInResponse + * @param returnUpdatedDocument + * @param upsert + * @param shredder + * @param updateLimit + */ public record ReadAndUpdateOperation( CommandContext commandContext, ReadOperation readOperation, @@ -26,47 +40,94 @@ public record ReadAndUpdateOperation( boolean returnDocumentInResponse, boolean returnUpdatedDocument, boolean upsert, - Shredder shredder) + Shredder shredder, + int updateLimit) implements ModifyOperation { @Override public Uni> execute(QueryExecutor queryExecutor) { - Uni docsToUpate = readOperation().getDocuments(queryExecutor, null); + final AtomicBoolean moreDataFlag = new AtomicBoolean(false); + final Multi findResponses = + Multi.createBy() + .repeating() + .uni( + () -> new AtomicReference(null), + stateRef -> { + Uni docsToUpdate = + readOperation().getDocuments(queryExecutor, stateRef.get()); + return docsToUpdate + .onItem() + .invoke(findResponse -> stateRef.set(findResponse.pagingState())); + }) + .whilst(findResponse -> findResponse.pagingState() != null); + final AtomicInteger matchedCount = new AtomicInteger(0); + final AtomicInteger modifiedCount = new AtomicInteger(0); + final Uni> updatedDocuments = - docsToUpate + findResponses .onItem() .transformToMulti( findResponse -> { - if (findResponse.docs().isEmpty()) { - if (upsert) { - return Multi.createFrom().item(readOperation().getEmptyDocuments()); + final List docs = findResponse.docs(); + if (upsert() && docs.size() == 0 && matchedCount.get() == 0) { + return Multi.createFrom().item(readOperation().getNewDocument()); + } else { + // Below conditionality is because we read up to deleteLimit +1 record. + if (matchedCount.get() + docs.size() <= updateLimit) { + matchedCount.addAndGet(docs.size()); + return Multi.createFrom().items(docs.stream()); } else { - return Multi.createFrom().items(Stream.empty()); + int needed = updateLimit - matchedCount.get(); + matchedCount.addAndGet(needed); + + moreDataFlag.set(true); + return Multi.createFrom() + .items(findResponse.docs().subList(0, needed).stream()); } - } else { - return Multi.createFrom().items(findResponse.docs().stream()); } }) + .concatenate() .onItem() .transformToUniAndConcatenate( readDocument -> { - JsonNode updatedDocument = + DocumentUpdater.DocumentUpdaterResponse documentUpdaterResponse = documentUpdater().applyUpdates(readDocument.document().deepCopy()); - WritableShreddedDocument writableShreddedDocument = - shredder().shred(updatedDocument, readDocument.txnId()); final JsonNode originalDocument = readDocument.txnId() == null ? null : readDocument.document(); + JsonNode updatedDocument = documentUpdaterResponse.document(); + Uni updated = Uni.createFrom().nullItem(); + if (documentUpdaterResponse.modified()) { + WritableShreddedDocument writableShreddedDocument = + shredder().shred(updatedDocument, readDocument.txnId()); + updated = updatedDocument(queryExecutor, writableShreddedDocument); + } final JsonNode documentToReturn = returnUpdatedDocument ? updatedDocument : originalDocument; - return updatedDocument(queryExecutor, writableShreddedDocument) + return updated .onItem() - .transform(v -> new UpdatedDocument(readDocument.id(), documentToReturn)); + .ifNotNull() + .transform( + v -> { + if (readDocument.txnId() != null) modifiedCount.incrementAndGet(); + return new UpdatedDocument( + readDocument.id(), + readDocument.txnId() == null, + returnDocumentInResponse ? documentToReturn : null); + }); }) .collect() .asList(); + return updatedDocuments .onItem() - .transform(updates -> new UpdateOperationPage(updates, returnDocumentInResponse())); + .transform( + updates -> + new UpdateOperationPage( + matchedCount.get(), + modifiedCount.get(), + updates, + returnDocumentInResponse(), + moreDataFlag.get())); } private Uni updatedDocument( @@ -133,5 +194,5 @@ protected static QueryOuterClass.Query bindUpdateValues( return QueryOuterClass.Query.newBuilder(builtQuery).setValues(values).build(); } - record UpdatedDocument(DocumentId id, JsonNode document) {} + record UpdatedDocument(DocumentId id, boolean upserted, JsonNode document) {} } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/UpdateOperationPage.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/UpdateOperationPage.java index fdf0c905d9..7474c3af2b 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/UpdateOperationPage.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/UpdateOperationPage.java @@ -5,28 +5,35 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandStatus; import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; -import java.util.Map; import java.util.function.Supplier; public record UpdateOperationPage( - List updatedDocuments, boolean returnDocs) + int matchedCount, + int modifiedCount, + List updatedDocuments, + boolean returnDocs, + boolean moreDataFlag) implements Supplier { @Override public CommandResult get() { - List updatedIds = new ArrayList<>(updatedDocuments().size()); + final DocumentId[] upsertedId = new DocumentId[1]; List updatedDocs = new ArrayList<>(updatedDocuments().size()); updatedDocuments.forEach( update -> { - updatedIds.add(update.id()); - updatedDocs.add(update.document()); + if (update.upserted()) upsertedId[0] = update.id(); + if (returnDocs) updatedDocs.add(update.document()); }); + EnumMap updateStatus = new EnumMap<>(CommandStatus.class); + if (upsertedId[0] != null) updateStatus.put(CommandStatus.UPSERTED_ID, upsertedId[0]); + updateStatus.put(CommandStatus.MATCHED_COUNT, matchedCount()); + updateStatus.put(CommandStatus.MODIFIED_COUNT, modifiedCount()); + if (moreDataFlag) updateStatus.put(CommandStatus.MORE_DATA, moreDataFlag); if (returnDocs) { - return new CommandResult( - new CommandResult.ResponseData(updatedDocs), - Map.of(CommandStatus.UPDATED_IDS, updatedIds)); + return new CommandResult(new CommandResult.ResponseData(updatedDocs), updateStatus); } else { - return new CommandResult(Map.of(CommandStatus.UPDATED_IDS, updatedIds)); + return new CommandResult(updateStatus); } } } 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 00f31633a2..9d25836517 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 @@ -39,7 +39,7 @@ public Operation resolveCommand(CommandContext ctx, FindOneAndUpdateCommand comm command.options() != null && "after".equals(command.options().returnDocument()); boolean upsert = command.options() != null && command.options().upsert(); return new ReadAndUpdateOperation( - ctx, readOperation, documentUpdater, true, returnUpdatedDocument, upsert, shredder); + ctx, readOperation, documentUpdater, true, returnUpdatedDocument, upsert, shredder, 1); } @Override diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java new file mode 100644 index 0000000000..a1c5034e4d --- /dev/null +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolver.java @@ -0,0 +1,63 @@ +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.UpdateManyCommand; +import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateOneCommand; +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.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; +import io.stargate.sgv2.jsonapi.service.shredding.Shredder; +import io.stargate.sgv2.jsonapi.service.updater.DocumentUpdater; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +/** Resolves the {@link UpdateOneCommand } */ +@ApplicationScoped +public class UpdateManyCommandResolver extends FilterableResolver + implements CommandResolver { + private Shredder shredder; + private final DocumentConfig documentConfig; + + @Inject + public UpdateManyCommandResolver( + ObjectMapper objectMapper, Shredder shredder, DocumentConfig documentConfig) { + super(objectMapper); + this.shredder = shredder; + this.documentConfig = documentConfig; + } + + @Override + public Class getCommandClass() { + return UpdateManyCommand.class; + } + + @Override + public Operation resolveCommand(CommandContext ctx, UpdateManyCommand command) { + ReadOperation readOperation = resolve(ctx, command); + DocumentUpdater documentUpdater = DocumentUpdater.construct(command.updateClause()); + boolean upsert = command.options() != null && command.options().upsert(); + return new ReadAndUpdateOperation( + ctx, + readOperation, + documentUpdater, + false, + false, + upsert, + shredder, + documentConfig.maxDocumentUpdateCount()); + } + + @Override + protected FilteringOptions getFilteringOption(UpdateManyCommand command) { + return new FilteringOptions( + documentConfig.maxDocumentUpdateCount() + 1, + null, + documentConfig.defaultPageSize(), + 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 180d0e8a33..1065c73319 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 @@ -37,7 +37,7 @@ public Operation resolveCommand(CommandContext ctx, UpdateOneCommand command) { DocumentUpdater documentUpdater = DocumentUpdater.construct(command.updateClause()); boolean upsert = command.options() != null && command.options().upsert(); return new ReadAndUpdateOperation( - ctx, readOperation, documentUpdater, false, false, upsert, shredder); + ctx, readOperation, documentUpdater, false, false, upsert, shredder, 1); } @Override diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdater.java b/src/main/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdater.java index 9b363f65d8..882fbdcde5 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdater.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdater.java @@ -13,10 +13,14 @@ public static DocumentUpdater construct(UpdateClause updateDef) { return new DocumentUpdater(updateDef.buildOperations()); } - public JsonNode applyUpdates(JsonNode readDocument) { + public DocumentUpdaterResponse applyUpdates(JsonNode readDocument) { UpdateTargetLocator targetLocator = new UpdateTargetLocator(); ObjectNode docToUpdate = (ObjectNode) readDocument; - updateOperations.forEach(u -> u.updateDocument(docToUpdate, targetLocator)); - return readDocument; + boolean modified = false; + for (UpdateOperation updateOperation : updateOperations) + modified |= updateOperation.updateDocument(docToUpdate, targetLocator); + return new DocumentUpdaterResponse(readDocument, modified); } + + public record DocumentUpdaterResponse(JsonNode document, boolean modified) {} } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndUpdateIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndUpdateIntegrationTest.java index 06ed623cf0..e56ba890a3 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndUpdateIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindAndUpdateIntegrationTest.java @@ -67,7 +67,8 @@ public void findByIdAndSet() { .then() .statusCode(200) .body("data.docs[0]", jsonEquals(expected)) - .body("status.updatedIds[0]", is("doc3")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); expected = "{\"_id\":\"doc3\", \"username\":\"user3\", \"active_user\":false}"; json = @@ -135,7 +136,8 @@ public void findByIdReturnDocumentAfter() { .then() .statusCode(200) .body("data.docs[0]", jsonEquals(expected)) - .body("status.updatedIds[0]", is("afterDoc3")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); json = """ @@ -179,7 +181,9 @@ public void findByIdUpsert() { .then() .statusCode(200) .body("data.docs[0]", jsonEquals(expected)) - .body("status.updatedIds[0]", is("afterDoc4")); + .body("status.upsertedId", is("afterDoc4")) + .body("status.matchedCount", is(0)) + .body("status.modifiedCount", is(0)); json = """ @@ -242,7 +246,8 @@ public void findByColumnAndSet() { .then() .statusCode(200) .body("data.docs[0]", jsonEquals(expected)) - .body("status.updatedIds[0]", is("doc4")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); expected = "{\"_id\":\"doc4\", \"username\":\"user4\", \"new_col\": \"new_val\"}"; json = @@ -308,7 +313,8 @@ public void findByIdAndUnset() { .then() .statusCode(200) .body("data.docs[0]", jsonEquals(expected)) - .body("status.updatedIds[0]", is("doc5")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); expected = "{\"_id\":\"doc5\", \"username\":\"user5\"}"; json = @@ -376,7 +382,9 @@ public void findByIdAndSet() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc1")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); + ; String expected = "{\"_id\":\"update_doc1\", \"username\":\"update_user3\", \"active_user\":false}"; @@ -421,7 +429,9 @@ public void findByIdUpsert() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("afterDoc6")); + .body("status.upsertedId", is("afterDoc6")) + .body("status.matchedCount", is(0)) + .body("status.modifiedCount", is(0)); json = """ @@ -483,7 +493,8 @@ public void findByColumnAndSet() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc2")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expected = "{\"_id\":\"update_doc2\", \"username\":\"update_user2\", \"new_col\": \"new_val\"}"; @@ -535,7 +546,9 @@ public void findByIdAndUnset() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc3")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); + ; String expected = "{\"_id\":\"update_doc3\", \"username\":\"update_user3\"}"; json = @@ -597,7 +610,8 @@ public void findByColumnAndSetArray() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc4")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expected = "{\"_id\":\"update_doc4\", \"username\":\"update_user4\", \"new_col\": [\"new_val\", \"new_val2\"]}"; @@ -660,7 +674,8 @@ public void findByColumnAndSetSubDoc() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc5")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expected = "{\"_id\":\"update_doc5\", \"username\":\"update_user5\", \"new_col\": {\"sub_doc_col\":\"new_val2\"}}"; @@ -738,7 +753,8 @@ public void findByIdAndUnsetNested() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_unset_nested")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expected = """ @@ -812,7 +828,8 @@ public void findByIdAndSetNested() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_set_nested")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expected = """ @@ -1049,7 +1066,9 @@ public void findByColumnAndPop() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_pop")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); + ; given() .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) @@ -1110,7 +1129,9 @@ public void findByColumnAndPush() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_push")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); + ; String expected = "{\"_id\":\"update_doc_push\", \"array\": [2, 13]}"; json = @@ -1164,7 +1185,9 @@ public void findByColumnAndPushWithEach() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_push_each")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); + ; String expected = """ @@ -1223,7 +1246,8 @@ public void findByColumnAndPushWithEachAndPosition() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_push_each_position")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expected = """ @@ -1281,7 +1305,8 @@ public void findByColumnAndInc() { .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) .then() .statusCode(200) - .body("status.updatedIds[0]", is("update_doc_inc")); + .body("status.matchedCount", is(1)) + .body("status.modifiedCount", is(1)); String expectedDoc = "{\"_id\":\"update_doc_inc\", \"number\": 119, \"newProp\": 0.25 }"; String findJson = diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/UpdateManyIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/UpdateManyIntegrationTest.java new file mode 100644 index 0000000000..cd845731cf --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/UpdateManyIntegrationTest.java @@ -0,0 +1,342 @@ +package io.stargate.sgv2.jsonapi.api.v1; + +import static io.restassured.RestAssured.given; +import static io.stargate.sgv2.common.IntegrationTestUtils.getAuthToken; +import static net.javacrumbs.jsonunit.JsonMatchers.jsonEquals; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +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.AfterEach; +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 UpdateManyIntegrationTest extends CollectionResourceBaseIntegrationTest { + @Nested + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + class UpdateMany { + + private void insert(int countOfDocument) { + for (int i = 1; i <= countOfDocument; i++) { + String json = + """ + { + "insertOne": { + "document": { + "_id": "doc%s", + "username": "user%s", + "active_user" : true + } + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json.formatted(i, i)) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200); + } + } + + @Test + @Order(2) + public void updateManyById() { + insert(1); + String json = + """ + { + "updateMany": { + "filter" : {"_id" : "doc1"}, + "update" : {"$set" : {"active_user": 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.matchedCount", is(1)) + .body("status.modifiedCount", is(1)) + .body("status.moreData", nullValue()); + + String expected = "{\"_id\":\"doc1\", \"username\":\"user1\", \"active_user\":false}"; + json = + """ + { + "find": { + "filter" : {"_id" : "doc1"} + } + } + """; + 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("data.docs[0]", jsonEquals(expected)); + } + + @Test + @Order(3) + public void updateManyByColumn() { + insert(5); + String json = + """ + { + "updateMany": { + "filter" : {"active_user": true}, + "update" : {"$set" : {"active_user": 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.matchedCount", is(5)) + .body("status.modifiedCount", is(5)) + .body("status.moreData", nullValue()); + + String expected = "{\"_id\":\"doc1\", \"username\":\"user1\", \"active_user\":false}"; + json = + """ + { + "find": { + "filter" : {"_id" : "doc1"} + } + } + """; + 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("data.docs[0]", jsonEquals(expected)); + } + + @Test + @Order(4) + public void updateManyLimit() { + insert(20); + String json = + """ + { + "updateMany": { + "filter" : {"active_user": true}, + "update" : {"$set" : {"active_user": 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.matchedCount", is(20)) + .body("status.modifiedCount", is(20)) + .body("status.moreData", nullValue()); + + json = + """ + { + "find": { + "filter" : {"active_user": 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("data.count", is(20)); + } + + @Test + @Order(5) + public void deleteManyLimitMoreDataFlag() { + insert(25); + String json = + """ + { + "updateMany": { + "filter" : {"active_user" : true}, + "update" : {"$set" : {"active_user": 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.matchedCount", is(20)) + .body("status.modifiedCount", is(20)) + .body("status.moreData", is(true)); + + json = + """ + { + "find": { + "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("data.count", is(5)); + } + + @Test + @Order(6) + public void updateManyUpsert() { + insert(5); + String json = + """ + { + "updateMany": { + "filter" : {"_id": "doc6"}, + "update" : {"$set" : {"active_user": false}}, + "options" : {"upsert" : 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.upsertedId", is("doc6")) + .body("status.matchedCount", is(0)) + .body("status.modifiedCount", is(0)) + .body("status.moreData", nullValue()); + + String expected = "{\"_id\":\"doc6\", \"active_user\":false}"; + json = + """ + { + "find": { + "filter" : {"_id" : "doc6"} + } + } + """; + 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("data.docs[0]", jsonEquals(expected)); + } + + @Test + @Order(7) + public void updateManyByIdNoChange() { + insert(1); + String json = + """ + { + "updateMany": { + "filter" : {"_id" : "doc1"}, + "update" : {"$set" : {"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.matchedCount", is(1)) + .body("status.modifiedCount", is(0)) + .body("status.moreData", nullValue()); + + String expected = "{\"_id\":\"doc1\", \"username\":\"user1\", \"active_user\":true}"; + json = + """ + { + "find": { + "filter" : {"_id" : "doc1"} + } + } + """; + 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("data.docs[0]", jsonEquals(expected)); + } + + @AfterEach + public void cleanUpData() { + String json = + """ + { + "deleteMany": { + } + } + """; + given() + .header(HttpConstants.AUTHENTICATION_TOKEN_HEADER_NAME, getAuthToken()) + .contentType(ContentType.JSON) + .body(json) + .when() + .post(CollectionResource.BASE_PATH, keyspaceId.asInternal(), collectionName) + .then() + .statusCode(200); + } + } +} diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java index 3f1753f29d..bba916d688 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/operation/model/impl/ReadAndUpdateOperationTest.java @@ -56,20 +56,20 @@ public void findAndUpdate() throws Exception { UUID tx_id = UUID.randomUUID(); String doc1 = """ - { - "_id": "doc1", - "username": "user1" - } - """; + { + "_id": "doc1", + "username": "user1" + } + """; String doc1Updated = """ - { - "_id": "doc1", - "username": "user1", - "name" : "test" - } - """; + { + "_id": "doc1", + "username": "user1", + "name" : "test" + } + """; withQuery( collectionReadCql, Values.of(CustomValueSerializers.getDocumentIdValue(DocumentId.fromString("doc1")))) @@ -146,19 +146,19 @@ public void findAndUpdate() throws Exception { String updater = """ - { - "findOneAndUpdate": { - "filter": { - "_id": "doc1" - }, - "update": { - "$set": { - "name": "test" + { + "findOneAndUpdate": { + "filter": { + "_id": "doc1" + }, + "update": { + "$set": { + "name": "test" + } + } } } - } - } - """; + """; FindOneAndUpdateCommand findOneAndUpdateCommand = objectMapper.readValue(updater, FindOneAndUpdateCommand.class); @@ -177,7 +177,7 @@ public void findAndUpdate() throws Exception { DocumentUpdater.construct(findOneAndUpdateCommand.updateClause()); ReadAndUpdateOperation operation = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, true, false, false, shredder); + commandContext, readOperation, documentUpdater, true, false, false, shredder, 1); final Uni> execute = operation.execute(queryExecutor); final CommandResult commandResultSupplier = execute.subscribe().asCompletionStage().get().get(); @@ -187,12 +187,10 @@ public void findAndUpdate() throws Exception { .satisfies( commandResult -> { assertThat(commandResultSupplier.status()).isNotNull(); - assertThat(commandResultSupplier.status().get(CommandStatus.UPDATED_IDS)) - .isNotNull(); - assertThat( - (List) - commandResultSupplier.status().get(CommandStatus.UPDATED_IDS)) - .contains(new DocumentId.StringId("doc1")); + assertThat(commandResultSupplier.status().get(CommandStatus.MATCHED_COUNT)) + .isEqualTo(1); + assertThat(commandResultSupplier.status().get(CommandStatus.MODIFIED_COUNT)) + .isEqualTo(1); }); } } @@ -206,11 +204,11 @@ public void findAndUpdateUpsert() throws Exception { UUID tx_id = UUID.randomUUID(); String doc1Updated = """ - { - "_id": "doc1", - "name" : "test" - } - """; + { + "_id": "doc1", + "name" : "test" + } + """; withQuery( collectionReadCql, Values.of(CustomValueSerializers.getDocumentIdValue(DocumentId.fromString("doc1")))) @@ -279,19 +277,19 @@ public void findAndUpdateUpsert() throws Exception { String updater = """ - { - "findOneAndUpdate": { - "filter": { - "_id": "doc1" - }, - "update": { - "$set": { - "name": "test" + { + "findOneAndUpdate": { + "filter": { + "_id": "doc1" + }, + "update": { + "$set": { + "name": "test" + } + } } } - } - } - """; + """; FindOneAndUpdateCommand findOneAndUpdateCommand = objectMapper.readValue(updater, FindOneAndUpdateCommand.class); @@ -310,7 +308,7 @@ public void findAndUpdateUpsert() throws Exception { DocumentUpdater.construct(findOneAndUpdateCommand.updateClause()); ReadAndUpdateOperation operation = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, true, false, true, shredder); + commandContext, readOperation, documentUpdater, true, false, true, shredder, 1); final Uni> execute = operation.execute(queryExecutor); final CommandResult commandResultSupplier = execute.subscribe().asCompletionStage().get().get(); UniAssertSubscriber> subscriber = @@ -319,11 +317,12 @@ public void findAndUpdateUpsert() throws Exception { .satisfies( commandResult -> { assertThat(commandResultSupplier.status()).isNotNull(); - assertThat(commandResultSupplier.status().get(CommandStatus.UPDATED_IDS)).isNotNull(); - assertThat( - (List) - commandResultSupplier.status().get(CommandStatus.UPDATED_IDS)) - .contains(new DocumentId.StringId("doc1")); + assertThat(commandResultSupplier.status().get(CommandStatus.MATCHED_COUNT)) + .isEqualTo(0); + assertThat(commandResultSupplier.status().get(CommandStatus.MODIFIED_COUNT)) + .isEqualTo(0); + assertThat((DocumentId) commandResultSupplier.status().get(CommandStatus.UPSERTED_ID)) + .isEqualTo(new DocumentId.StringId("doc1")); }); } @@ -356,19 +355,19 @@ public void findAndUpdateNoData() throws Exception { String updater = """ - { - "findOneAndUpdate": { - "filter": { - "_id": "doc1" - }, - "update": { - "$set": { - "name": "test" + { + "findOneAndUpdate": { + "filter": { + "_id": "doc1" + }, + "update": { + "$set": { + "name": "test" + } + } } } - } - } - """; + """; FindOneAndUpdateCommand findOneAndUpdateCommand = objectMapper.readValue(updater, FindOneAndUpdateCommand.class); @@ -387,7 +386,409 @@ public void findAndUpdateNoData() throws Exception { DocumentUpdater.construct(findOneAndUpdateCommand.updateClause()); ReadAndUpdateOperation operation = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, true, false, false, shredder); + commandContext, readOperation, documentUpdater, true, false, false, shredder, 1); + final Uni> execute = operation.execute(queryExecutor); + final CommandResult commandResultSupplier = execute.subscribe().asCompletionStage().get().get(); + UniAssertSubscriber> subscriber = + operation.execute(queryExecutor).subscribe().withSubscriber(UniAssertSubscriber.create()); + assertThat(commandResultSupplier) + .satisfies( + commandResult -> { + assertThat(commandResultSupplier.status()).isNotNull(); + assertThat(commandResultSupplier.status().get(CommandStatus.MATCHED_COUNT)) + .isEqualTo(0); + assertThat(commandResultSupplier.status().get(CommandStatus.MODIFIED_COUNT)) + .isEqualTo(0); + }); + } + + @Test + public void findAndUpdateMany() throws Exception { + String collectionReadCql = + "SELECT key, tx_id, doc_json FROM \"%s\".\"%s\" WHERE array_contains CONTAINS ? LIMIT 21" + .formatted(KEYSPACE_NAME, COLLECTION_NAME); + + UUID tx_id1 = UUID.randomUUID(); + UUID tx_id2 = UUID.randomUUID(); + String doc1 = + """ + { + "_id": "doc1", + "username": "user1", + "status" : "active" + } + """; + + String doc1Updated = + """ + { + "_id": "doc1", + "username": "user1", + "status" : "active", + "name" : "test" + } + """; + + String doc2 = + """ + { + "_id": "doc2", + "username": "user2", + "status" : "active" + } + """; + + String doc2Updated = + """ + { + "_id": "doc2", + "username": "user2", + "status" : "active", + "name" : "test" + } + """; + withQuery(collectionReadCql, Values.of("status Sactive")) + .withPageSize(20) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("key") + .setType(TypeSpecs.tuple(TypeSpecs.TINYINT, TypeSpecs.VARCHAR)) + .build(), + QueryOuterClass.ColumnSpec.newBuilder() + .setName("tx_id") + .setType(TypeSpecs.UUID) + .build(), + QueryOuterClass.ColumnSpec.newBuilder() + .setName("doc_json") + .setType(TypeSpecs.VARCHAR) + .build())) + .returning( + List.of( + List.of( + Values.of( + CustomValueSerializers.getDocumentIdValue(DocumentId.fromString("doc1"))), + Values.of(tx_id1), + Values.of(doc1)), + List.of( + Values.of( + CustomValueSerializers.getDocumentIdValue(DocumentId.fromString("doc2"))), + Values.of(tx_id2), + Values.of(doc2)))); + + String update = + "UPDATE %s.%s " + + " SET" + + " tx_id = now()," + + " doc_properties = ?," + + " exist_keys = ?," + + " sub_doc_equals = ?," + + " array_size = ?," + + " array_equals = ?," + + " array_contains = ?," + + " query_bool_values = ?," + + " query_dbl_values = ?," + + " query_text_values = ?," + + " query_null_values = ?," + + " doc_json = ?" + + " WHERE " + + " key = ?" + + " IF " + + " tx_id = ?"; + String collectionUpdateCql = update.formatted(KEYSPACE_NAME, COLLECTION_NAME); + JsonNode jsonNode = objectMapper.readTree(doc1Updated); + WritableShreddedDocument shredDocument = shredder.shred(jsonNode); + + withQuery( + collectionUpdateCql, + Values.of(CustomValueSerializers.getIntegerMapValues(shredDocument.docProperties())), + Values.of(CustomValueSerializers.getSetValue(shredDocument.existKeys())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.subDocEquals())), + Values.of(CustomValueSerializers.getIntegerMapValues(shredDocument.arraySize())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.arrayEquals())), + Values.of(CustomValueSerializers.getStringSetValue(shredDocument.arrayContains())), + Values.of(CustomValueSerializers.getBooleanMapValues(shredDocument.queryBoolValues())), + Values.of(CustomValueSerializers.getDoubleMapValues(shredDocument.queryNumberValues())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.queryTextValues())), + Values.of(CustomValueSerializers.getSetValue(shredDocument.queryNullValues())), + Values.of(shredDocument.docJson()), + Values.of(CustomValueSerializers.getDocumentIdValue(shredDocument.id())), + Values.of(tx_id1)) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("applied") + .setType(TypeSpecs.BOOLEAN) + .build())) + .returning(List.of(List.of(Values.of(true)))); + + jsonNode = objectMapper.readTree(doc2Updated); + shredDocument = shredder.shred(jsonNode); + + withQuery( + collectionUpdateCql, + Values.of(CustomValueSerializers.getIntegerMapValues(shredDocument.docProperties())), + Values.of(CustomValueSerializers.getSetValue(shredDocument.existKeys())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.subDocEquals())), + Values.of(CustomValueSerializers.getIntegerMapValues(shredDocument.arraySize())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.arrayEquals())), + Values.of(CustomValueSerializers.getStringSetValue(shredDocument.arrayContains())), + Values.of(CustomValueSerializers.getBooleanMapValues(shredDocument.queryBoolValues())), + Values.of(CustomValueSerializers.getDoubleMapValues(shredDocument.queryNumberValues())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.queryTextValues())), + Values.of(CustomValueSerializers.getSetValue(shredDocument.queryNullValues())), + Values.of(shredDocument.docJson()), + Values.of(CustomValueSerializers.getDocumentIdValue(shredDocument.id())), + Values.of(tx_id2)) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("applied") + .setType(TypeSpecs.BOOLEAN) + .build())) + .returning(List.of(List.of(Values.of(true)))); + + String updater = + """ + { + "findOneAndUpdate": { + "filter": { + "_id": "doc1" + }, + "update": { + "$set": { + "name": "test" + } + } + } + } + """; + + FindOneAndUpdateCommand findOneAndUpdateCommand = + objectMapper.readValue(updater, FindOneAndUpdateCommand.class); + ReadOperation readOperation = + new FindOperation( + commandContext, + List.of( + new DBFilterBase.TextFilter( + "status", DBFilterBase.MapFilterBase.Operator.EQ, "active")), + null, + 21, + 20, + ReadType.DOCUMENT, + objectMapper); + DocumentUpdater documentUpdater = + DocumentUpdater.construct(findOneAndUpdateCommand.updateClause()); + ReadAndUpdateOperation operation = + new ReadAndUpdateOperation( + commandContext, readOperation, documentUpdater, true, false, false, shredder, 20); + final Uni> execute = operation.execute(queryExecutor); + final CommandResult commandResultSupplier = execute.subscribe().asCompletionStage().get().get(); + UniAssertSubscriber> subscriber = + operation.execute(queryExecutor).subscribe().withSubscriber(UniAssertSubscriber.create()); + assertThat(commandResultSupplier) + .satisfies( + commandResult -> { + assertThat(commandResultSupplier.status()).isNotNull(); + assertThat(commandResultSupplier.status().get(CommandStatus.MATCHED_COUNT)) + .isEqualTo(2); + assertThat(commandResultSupplier.status().get(CommandStatus.MODIFIED_COUNT)) + .isEqualTo(2); + }); + } + + @Test + public void findAndUpdateManyUpsert() throws Exception { + String collectionReadCql = + "SELECT key, tx_id, doc_json FROM \"%s\".\"%s\" WHERE key = ? LIMIT 21" + .formatted(KEYSPACE_NAME, COLLECTION_NAME); + + UUID tx_id = UUID.randomUUID(); + String doc1Updated = + """ + { + "_id": "doc1", + "name" : "test" + } + """; + withQuery( + collectionReadCql, + Values.of(CustomValueSerializers.getDocumentIdValue(DocumentId.fromString("doc1")))) + .withPageSize(20) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("key") + .setType(TypeSpecs.tuple(TypeSpecs.TINYINT, TypeSpecs.VARCHAR)) + .build(), + QueryOuterClass.ColumnSpec.newBuilder() + .setName("tx_id") + .setType(TypeSpecs.UUID) + .build(), + QueryOuterClass.ColumnSpec.newBuilder() + .setName("doc_json") + .setType(TypeSpecs.VARCHAR) + .build())) + .returning(List.of()); + + String update = + "UPDATE %s.%s " + + " SET" + + " tx_id = now()," + + " doc_properties = ?," + + " exist_keys = ?," + + " sub_doc_equals = ?," + + " array_size = ?," + + " array_equals = ?," + + " array_contains = ?," + + " query_bool_values = ?," + + " query_dbl_values = ?," + + " query_text_values = ?," + + " query_null_values = ?," + + " doc_json = ?" + + " WHERE " + + " key = ?" + + " IF " + + " tx_id = ?"; + String collectionUpdateCql = update.formatted(KEYSPACE_NAME, COLLECTION_NAME); + final JsonNode jsonNode = objectMapper.readTree(doc1Updated); + final WritableShreddedDocument shredDocument = shredder.shred(jsonNode); + + withQuery( + collectionUpdateCql, + Values.of(CustomValueSerializers.getIntegerMapValues(shredDocument.docProperties())), + Values.of(CustomValueSerializers.getSetValue(shredDocument.existKeys())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.subDocEquals())), + Values.of(CustomValueSerializers.getIntegerMapValues(shredDocument.arraySize())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.arrayEquals())), + Values.of(CustomValueSerializers.getStringSetValue(shredDocument.arrayContains())), + Values.of(CustomValueSerializers.getBooleanMapValues(shredDocument.queryBoolValues())), + Values.of(CustomValueSerializers.getDoubleMapValues(shredDocument.queryNumberValues())), + Values.of(CustomValueSerializers.getStringMapValues(shredDocument.queryTextValues())), + Values.of(CustomValueSerializers.getSetValue(shredDocument.queryNullValues())), + Values.of(shredDocument.docJson()), + Values.of(CustomValueSerializers.getDocumentIdValue(shredDocument.id())), + Values.NULL) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("applied") + .setType(TypeSpecs.BOOLEAN) + .build())) + .returning(List.of(List.of(Values.of(true)))); + + String updater = + """ + { + "findOneAndUpdate": { + "filter": { + "_id": "doc1" + }, + "update": { + "$set": { + "name": "test" + } + } + } + } + """; + + FindOneAndUpdateCommand findOneAndUpdateCommand = + objectMapper.readValue(updater, FindOneAndUpdateCommand.class); + ReadOperation readOperation = + new FindOperation( + commandContext, + List.of( + new DBFilterBase.IDFilter( + DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1"))), + null, + 21, + 20, + ReadType.DOCUMENT, + objectMapper); + DocumentUpdater documentUpdater = + DocumentUpdater.construct(findOneAndUpdateCommand.updateClause()); + ReadAndUpdateOperation operation = + new ReadAndUpdateOperation( + commandContext, readOperation, documentUpdater, true, false, true, shredder, 20); + final Uni> execute = operation.execute(queryExecutor); + final CommandResult commandResultSupplier = execute.subscribe().asCompletionStage().get().get(); + UniAssertSubscriber> subscriber = + operation.execute(queryExecutor).subscribe().withSubscriber(UniAssertSubscriber.create()); + assertThat(commandResultSupplier) + .satisfies( + commandResult -> { + assertThat(commandResultSupplier.status()).isNotNull(); + assertThat(commandResultSupplier.status().get(CommandStatus.MATCHED_COUNT)) + .isEqualTo(0); + assertThat(commandResultSupplier.status().get(CommandStatus.MODIFIED_COUNT)) + .isEqualTo(0); + assertThat((DocumentId) commandResultSupplier.status().get(CommandStatus.UPSERTED_ID)) + .isEqualTo(new DocumentId.StringId("doc1")); + }); + } + + @Test + public void findAndUpdateManyNoData() throws Exception { + String collectionReadCql = + "SELECT key, tx_id, doc_json FROM \"%s\".\"%s\" WHERE key = ? LIMIT 21" + .formatted(KEYSPACE_NAME, COLLECTION_NAME); + + UUID tx_id = UUID.randomUUID(); + withQuery( + collectionReadCql, + Values.of(CustomValueSerializers.getDocumentIdValue(DocumentId.fromString("doc1")))) + .withPageSize(20) + .withColumnSpec( + List.of( + QueryOuterClass.ColumnSpec.newBuilder() + .setName("key") + .setType(TypeSpecs.tuple(TypeSpecs.TINYINT, TypeSpecs.VARCHAR)) + .build(), + QueryOuterClass.ColumnSpec.newBuilder() + .setName("tx_id") + .setType(TypeSpecs.UUID) + .build(), + QueryOuterClass.ColumnSpec.newBuilder() + .setName("doc_json") + .setType(TypeSpecs.VARCHAR) + .build())) + .returning(List.of()); + + String updater = + """ + { + "findOneAndUpdate": { + "filter": { + "_id": "doc1" + }, + "update": { + "$set": { + "name": "test" + } + } + } + } + """; + + FindOneAndUpdateCommand findOneAndUpdateCommand = + objectMapper.readValue(updater, FindOneAndUpdateCommand.class); + ReadOperation readOperation = + new FindOperation( + commandContext, + List.of( + new DBFilterBase.IDFilter( + DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("doc1"))), + null, + 21, + 20, + ReadType.DOCUMENT, + objectMapper); + DocumentUpdater documentUpdater = + DocumentUpdater.construct(findOneAndUpdateCommand.updateClause()); + ReadAndUpdateOperation operation = + new ReadAndUpdateOperation( + commandContext, readOperation, documentUpdater, true, false, false, shredder, 20); final Uni> execute = operation.execute(queryExecutor); final CommandResult commandResultSupplier = execute.subscribe().asCompletionStage().get().get(); UniAssertSubscriber> subscriber = @@ -396,11 +797,10 @@ public void findAndUpdateNoData() throws Exception { .satisfies( commandResult -> { assertThat(commandResultSupplier.status()).isNotNull(); - assertThat(commandResultSupplier.status().get(CommandStatus.UPDATED_IDS)).isNotNull(); - assertThat( - (List) - commandResultSupplier.status().get(CommandStatus.UPDATED_IDS)) - .hasSize(0); + assertThat(commandResultSupplier.status().get(CommandStatus.MATCHED_COUNT)) + .isEqualTo(0); + assertThat(commandResultSupplier.status().get(CommandStatus.MODIFIED_COUNT)) + .isEqualTo(0); }); } } 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 6f743524be..c8471425cf 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 @@ -70,7 +70,7 @@ public void idFilterConditionBsonType() throws Exception { objectMapper.getNodeFactory().objectNode().put("location", "New York"))); ReadAndUpdateOperation expected = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, true, false, false, shredder); + commandContext, readOperation, documentUpdater, true, false, false, shredder, 1); assertThat(operation) .isInstanceOf(ReadAndUpdateOperation.class) .satisfies( @@ -116,7 +116,7 @@ public void idFilterConditionWithOptions() throws Exception { objectMapper.getNodeFactory().objectNode().put("location", "New York"))); ReadAndUpdateOperation expected = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, true, true, true, shredder); + commandContext, readOperation, documentUpdater, true, true, true, shredder, 1); assertThat(operation) .isInstanceOf(ReadAndUpdateOperation.class) .satisfies( @@ -161,7 +161,7 @@ public void dynamicFilterCondition() throws Exception { objectMapper.getNodeFactory().objectNode().put("location", "New York"))); ReadAndUpdateOperation expected = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, true, false, false, shredder); + commandContext, readOperation, documentUpdater, true, false, false, shredder, 1); assertThat(operation) .isInstanceOf(ReadAndUpdateOperation.class) .satisfies( diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolverTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolverTest.java new file mode 100644 index 0000000000..a2d1f0907a --- /dev/null +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/resolver/model/impl/UpdateManyCommandResolverTest.java @@ -0,0 +1,189 @@ +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.clause.update.UpdateOperator; +import io.stargate.sgv2.jsonapi.api.model.command.impl.UpdateManyCommand; +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.operation.model.impl.ReadAndUpdateOperation; +import io.stargate.sgv2.jsonapi.service.shredding.Shredder; +import io.stargate.sgv2.jsonapi.service.shredding.model.DocumentId; +import io.stargate.sgv2.jsonapi.service.testutil.DocumentUpdaterUtils; +import io.stargate.sgv2.jsonapi.service.updater.DocumentUpdater; +import java.util.List; +import javax.inject.Inject; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(NoGlobalResourcesTestProfile.Impl.class) +public class UpdateManyCommandResolverTest { + @Inject ObjectMapper objectMapper; + @Inject DocumentConfig documentConfig; + @Inject Shredder shredder; + + @Inject UpdateManyCommandResolver updateManyCommandResolver; + + @Nested + class UpdateManyCommandResolveCommand { + + @Test + public void idFilterCondition() throws Exception { + String json = + """ + { + "updateMany": { + "filter" : {"_id" : "id"}, + "update" : {"$set" : {"location" : "New York"}} + } + } + """; + + UpdateManyCommand updateManyCommand = objectMapper.readValue(json, UpdateManyCommand.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = + updateManyCommandResolver.resolveCommand(commandContext, updateManyCommand); + FindOperation readOperation = + new FindOperation( + commandContext, + List.of( + new DBFilterBase.IDFilter( + DBFilterBase.IDFilter.Operator.EQ, DocumentId.fromString("id"))), + null, + documentConfig.maxDocumentUpdateCount() + 1, + documentConfig.defaultPageSize(), + ReadType.DOCUMENT, + objectMapper); + DocumentUpdater documentUpdater = + DocumentUpdater.construct( + DocumentUpdaterUtils.updateClause( + UpdateOperator.SET, + objectMapper.getNodeFactory().objectNode().put("location", "New York"))); + ReadAndUpdateOperation expected = + new ReadAndUpdateOperation( + commandContext, + readOperation, + documentUpdater, + false, + false, + false, + shredder, + documentConfig.maxDocumentUpdateCount()); + + assertThat(operation) + .isInstanceOf(ReadAndUpdateOperation.class) + .satisfies( + op -> { + assertThat(op).isEqualTo(expected); + }); + } + + @Test + public void noFilterCondition() throws Exception { + String json = + """ + { + "updateMany": { + "update" : {"$set" : {"location" : "New York"}} + } + } + """; + + UpdateManyCommand updateManyCommand = objectMapper.readValue(json, UpdateManyCommand.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = + updateManyCommandResolver.resolveCommand(commandContext, updateManyCommand); + FindOperation readOperation = + new FindOperation( + commandContext, + List.of(), + null, + documentConfig.maxDocumentUpdateCount() + 1, + documentConfig.defaultPageSize(), + ReadType.DOCUMENT, + objectMapper); + DocumentUpdater documentUpdater = + DocumentUpdater.construct( + DocumentUpdaterUtils.updateClause( + UpdateOperator.SET, + objectMapper.getNodeFactory().objectNode().put("location", "New York"))); + ReadAndUpdateOperation expected = + new ReadAndUpdateOperation( + commandContext, + readOperation, + documentUpdater, + false, + false, + false, + shredder, + documentConfig.maxDocumentUpdateCount()); + + assertThat(operation) + .isInstanceOf(ReadAndUpdateOperation.class) + .satisfies( + op -> { + assertThat(op).isEqualTo(expected); + }); + } + + @Test + public void dynamicFilterCondition() throws Exception { + String json = + """ + { + "updateMany": { + "filter" : {"col" : "val"}, + "update" : {"$set" : {"location" : "New York"}} + } + } + """; + + UpdateManyCommand updateManyCommand = objectMapper.readValue(json, UpdateManyCommand.class); + final CommandContext commandContext = new CommandContext("namespace", "collection"); + final Operation operation = + updateManyCommandResolver.resolveCommand(commandContext, updateManyCommand); + FindOperation readOperation = + new FindOperation( + commandContext, + List.of( + new DBFilterBase.TextFilter( + "col", DBFilterBase.MapFilterBase.Operator.EQ, "val")), + null, + documentConfig.maxDocumentUpdateCount() + 1, + documentConfig.defaultPageSize(), + ReadType.DOCUMENT, + objectMapper); + DocumentUpdater documentUpdater = + DocumentUpdater.construct( + DocumentUpdaterUtils.updateClause( + UpdateOperator.SET, + objectMapper.getNodeFactory().objectNode().put("location", "New York"))); + + ReadAndUpdateOperation expected = + new ReadAndUpdateOperation( + commandContext, + readOperation, + documentUpdater, + false, + false, + false, + shredder, + documentConfig.maxDocumentUpdateCount()); + assertThat(operation) + .isInstanceOf(ReadAndUpdateOperation.class) + .satisfies( + op -> { + assertThat(op).isEqualTo(expected); + }); + } + } +} 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 a6321e1395..46cb78f2de 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 @@ -69,7 +69,7 @@ public void idFilterConditionBsonType() throws Exception { objectMapper.getNodeFactory().objectNode().put("location", "New York"))); ReadAndUpdateOperation expected = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, false, false, false, shredder); + commandContext, readOperation, documentUpdater, false, false, false, shredder, 1); assertThat(operation) .isInstanceOf(ReadAndUpdateOperation.class) .satisfies( @@ -113,7 +113,7 @@ public void dynamicFilterCondition() throws Exception { objectMapper.getNodeFactory().objectNode().put("location", "New York"))); ReadAndUpdateOperation expected = new ReadAndUpdateOperation( - commandContext, readOperation, documentUpdater, false, false, false, shredder); + commandContext, readOperation, documentUpdater, false, false, false, shredder, 1); assertThat(operation) .isInstanceOf(ReadAndUpdateOperation.class) .satisfies( diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdaterTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdaterTest.java index acac24cd48..9740dd71b2 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdaterTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/updater/DocumentUpdaterTest.java @@ -25,11 +25,11 @@ public class DocumentUpdaterTest { private static String BASE_DOC_JSON = """ - { - "_id": "1", - "location": "London" - } - """; + { + "_id": "1", + "location": "London" + } + """; @Nested class UpdateDocumentHappy { @@ -38,11 +38,11 @@ class UpdateDocumentHappy { public void setUpdateCondition() throws Exception { String expected = """ - { - "_id": "1", - "location": "New York" - } - """; + { + "_id": "1", + "location": "New York" + } + """; JsonNode baseData = objectMapper.readTree(BASE_DOC_JSON); JsonNode expectedData = objectMapper.readTree(expected); @@ -51,12 +51,14 @@ public void setUpdateCondition() throws Exception { DocumentUpdaterUtils.updateClause( UpdateOperator.SET, objectMapper.getNodeFactory().objectNode().put("location", "New York"))); - JsonNode updatedDocument = documentUpdater.applyUpdates(baseData); + DocumentUpdater.DocumentUpdaterResponse updatedDocument = + documentUpdater.applyUpdates(baseData); assertThat(updatedDocument) .isNotNull() .satisfies( node -> { - assertThat(node).isEqualTo(expectedData); + assertThat(node.document()).isEqualTo(expectedData); + assertThat(node.modified()).isEqualTo(true); }); } @@ -64,12 +66,12 @@ public void setUpdateCondition() throws Exception { public void setUpdateNewData() throws Exception { String expected = """ - { - "_id": "1", - "location": "London", - "new_data" : "data" - } - """; + { + "_id": "1", + "location": "London", + "new_data" : "data" + } + """; JsonNode baseData = objectMapper.readTree(BASE_DOC_JSON); JsonNode expectedData = objectMapper.readTree(expected); @@ -78,12 +80,14 @@ public void setUpdateNewData() throws Exception { DocumentUpdaterUtils.updateClause( UpdateOperator.SET, objectMapper.getNodeFactory().objectNode().put("new_data", "data"))); - JsonNode updatedDocument = documentUpdater.applyUpdates(baseData); + DocumentUpdater.DocumentUpdaterResponse updatedDocument = + documentUpdater.applyUpdates(baseData); assertThat(updatedDocument) .isNotNull() .satisfies( node -> { - assertThat(node).isEqualTo(expectedData); + assertThat(node.document()).isEqualTo(expectedData); + assertThat(node.modified()).isEqualTo(true); }); } @@ -91,12 +95,12 @@ public void setUpdateNewData() throws Exception { public void setUpdateNumberData() throws Exception { String expected = """ - { - "_id": "1", - "location": "London", - "new_data" : 40 - } - """; + { + "_id": "1", + "location": "London", + "new_data" : 40 + } + """; JsonNode baseData = objectMapper.readTree(BASE_DOC_JSON); JsonNode expectedData = objectMapper.readTree(expected); @@ -105,12 +109,14 @@ public void setUpdateNumberData() throws Exception { DocumentUpdaterUtils.updateClause( UpdateOperator.SET, objectMapper.getNodeFactory().objectNode().put("new_data", 40))); - JsonNode updatedDocument = documentUpdater.applyUpdates(baseData); + DocumentUpdater.DocumentUpdaterResponse updatedDocument = + documentUpdater.applyUpdates(baseData); assertThat(updatedDocument) .isNotNull() .satisfies( node -> { - assertThat(node).isEqualTo(expectedData); + assertThat(node.document()).isEqualTo(expectedData); + assertThat(node.modified()).isEqualTo(true); }); } @@ -118,11 +124,11 @@ public void setUpdateNumberData() throws Exception { public void unsetUpdateData() throws Exception { String expected = """ - { - "_id": "1", - "location": "London" - } - """; + { + "_id": "1", + "location": "London" + } + """; ObjectNode baseData = (ObjectNode) objectMapper.readTree(BASE_DOC_JSON); baseData.put("col", "data"); @@ -131,12 +137,14 @@ public void unsetUpdateData() throws Exception { DocumentUpdater.construct( DocumentUpdaterUtils.updateClause( UpdateOperator.UNSET, objectMapper.getNodeFactory().objectNode().put("col", 1))); - JsonNode updatedDocument = documentUpdater.applyUpdates(baseData); + DocumentUpdater.DocumentUpdaterResponse updatedDocument = + documentUpdater.applyUpdates(baseData); assertThat(updatedDocument) .isNotNull() .satisfies( node -> { - assertThat(node).isEqualTo(expectedData); + assertThat(node.document()).isEqualTo(expectedData); + assertThat(node.modified()).isEqualTo(true); }); } }