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);
});
}
}