diff --git a/src/main/java/liquibase/ext/mongodb/change/UpdateManyChange.java b/src/main/java/liquibase/ext/mongodb/change/UpdateManyChange.java new file mode 100644 index 00000000..bb28896b --- /dev/null +++ b/src/main/java/liquibase/ext/mongodb/change/UpdateManyChange.java @@ -0,0 +1,42 @@ +package liquibase.ext.mongodb.change; + +import liquibase.change.ChangeMetaData; +import liquibase.change.CheckSum; +import liquibase.change.DatabaseChange; +import liquibase.database.Database; +import liquibase.ext.mongodb.statement.UpdateManyStatement; +import liquibase.statement.SqlStatement; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@DatabaseChange(name = "updateMany", + description = "Updates all documents that match the specified filter for a collection " + + "https://www.mongodb.com/docs/manual/reference/method/db.collection.updateMany", + priority = ChangeMetaData.PRIORITY_DEFAULT, appliesTo = "collection") +@NoArgsConstructor +@Getter +@Setter +public class UpdateManyChange extends AbstractMongoChange { + + private String collectionName; + private String filter; + private String update; + + @Override + public String getConfirmationMessage() { + return "Documents updated in collection " + getCollectionName(); + } + + @Override + public SqlStatement[] generateStatements(final Database database) { + return new SqlStatement[]{ + new UpdateManyStatement(collectionName, filter, update) + }; + } + + @Override + public CheckSum generateCheckSum() { + return super.generateCheckSum(collectionName, filter, update); + } +} diff --git a/src/main/java/liquibase/ext/mongodb/statement/BsonUtils.java b/src/main/java/liquibase/ext/mongodb/statement/BsonUtils.java index 7300bfc5..5a8a6b56 100644 --- a/src/main/java/liquibase/ext/mongodb/statement/BsonUtils.java +++ b/src/main/java/liquibase/ext/mongodb/statement/BsonUtils.java @@ -22,7 +22,9 @@ import com.mongodb.DBRefCodecProvider; import com.mongodb.MongoClientSettings; + import lombok.NoArgsConstructor; + import org.bson.Document; import org.bson.UuidRepresentation; import org.bson.codecs.*; @@ -30,13 +32,17 @@ import org.bson.codecs.configuration.CodecRegistry; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; + import static liquibase.util.StringUtil.trimToNull; import static lombok.AccessLevel.PRIVATE; + import static org.bson.codecs.configuration.CodecRegistries.fromProviders; +import org.bson.conversions.Bson; @NoArgsConstructor(access = PRIVATE) public final class BsonUtils { @@ -77,6 +83,16 @@ public static List orEmptyList(final String json) { ); } + public static Class classOf(final String json) { + return ( + ofNullable(trimToNull(json)) + .map(jn -> "{ " + ITEMS + ": " + jn + "}") + .map(s -> Document.parse(s, DOCUMENT_CODEC)) + .map(d -> d.get(ITEMS).getClass()) + .orElse(null) + ); + } + public static String toJson(final Document document) { return ofNullable(document).map(Document::toJson).orElse(null); } diff --git a/src/main/java/liquibase/ext/mongodb/statement/UpdateManyStatement.java b/src/main/java/liquibase/ext/mongodb/statement/UpdateManyStatement.java index 4711c5ee..8558335f 100644 --- a/src/main/java/liquibase/ext/mongodb/statement/UpdateManyStatement.java +++ b/src/main/java/liquibase/ext/mongodb/statement/UpdateManyStatement.java @@ -20,17 +20,24 @@ * #L% */ +import java.util.ArrayList; +import java.util.List; +import static java.util.Optional.ofNullable; + +import org.bson.Document; +import org.bson.conversions.Bson; + import com.mongodb.client.MongoCollection; + import liquibase.ext.mongodb.database.MongoLiquibaseDatabase; +import static liquibase.ext.mongodb.statement.AbstractRunCommandStatement.SHELL_DB_PREFIX; +import static liquibase.ext.mongodb.statement.BsonUtils.classOf; +import static liquibase.ext.mongodb.statement.BsonUtils.orEmptyDocument; +import static liquibase.ext.mongodb.statement.BsonUtils.orEmptyList; import liquibase.nosql.statement.NoSqlExecuteStatement; import liquibase.nosql.statement.NoSqlUpdateStatement; import lombok.EqualsAndHashCode; import lombok.Getter; -import org.bson.Document; -import org.bson.conversions.Bson; - -import static java.util.Optional.ofNullable; -import static liquibase.ext.mongodb.statement.AbstractRunCommandStatement.SHELL_DB_PREFIX; @Getter @EqualsAndHashCode(callSuper = true) @@ -40,12 +47,31 @@ public class UpdateManyStatement extends AbstractCollectionStatement public static final String COMMAND_NAME = "updateMany"; private final Bson filter; - private final Bson document; + private Bson update = null; + private List aggregation = null; + + public UpdateManyStatement(final String collectionName, final String filter, final String update) { + super(collectionName); + this.filter = orEmptyDocument(filter); + Class clazz = classOf(update); + + if (Document.class.equals(clazz)) { + this.update = orEmptyDocument(update); + } else if (ArrayList.class.equals(clazz)) { + this.aggregation = orEmptyList(update); + } + } - public UpdateManyStatement(final String collectionName, final Bson filter, final Bson document) { + public UpdateManyStatement(final String collectionName, final Bson filter, final Bson update) { super(collectionName); this.filter = filter; - this.document = document; + this.update = update; + } + + public UpdateManyStatement(final String collectionName, final Bson filter, final List aggregation) { + super(collectionName); + this.filter = filter; + this.aggregation = aggregation; } @Override @@ -55,15 +81,23 @@ public String getCommandName() { @Override public String toJs() { + String updateString = "{}"; + + if (update != null) { + updateString = update.toBsonDocument().toJson(); + } else if (aggregation != null) { + updateString = "[" + String.join(",", aggregation.stream().map(u -> u.toBsonDocument().toJson()).toArray(String[]::new)) + "]"; + } + return SHELL_DB_PREFIX + getCollectionName() + "." + getCommandName() + "(" + - ofNullable(filter).map(Bson::toString).orElse(null) + + ofNullable(filter).map(f -> f.toBsonDocument().toJson()).orElse(null) + ", " + - ofNullable(document).map(Bson::toString).orElse(null) + + updateString + ");"; } @@ -75,6 +109,13 @@ public void execute(final MongoLiquibaseDatabase database) { @Override public int update(final MongoLiquibaseDatabase database) { final MongoCollection collection = database.getMongoDatabase().getCollection(getCollectionName()); - return (int) collection.updateMany(filter, document).getMatchedCount(); + + if (update != null) { + return (int) collection.updateMany(filter, update).getMatchedCount(); + } else if (aggregation != null) { + return (int) collection.updateMany(filter, aggregation).getMatchedCount(); + } + + return 0; } } diff --git a/src/main/resources/META-INF/services/liquibase.change.Change b/src/main/resources/META-INF/services/liquibase.change.Change index e14a9f03..b3bfcf38 100644 --- a/src/main/resources/META-INF/services/liquibase.change.Change +++ b/src/main/resources/META-INF/services/liquibase.change.Change @@ -6,3 +6,4 @@ liquibase.ext.mongodb.change.DropIndexChange liquibase.ext.mongodb.change.InsertManyChange liquibase.ext.mongodb.change.InsertOneChange liquibase.ext.mongodb.change.RunCommandChange +liquibase.ext.mongodb.change.UpdateManyChange \ No newline at end of file diff --git a/src/main/resources/www.liquibase.org/xml/ns/mongodb/liquibase-mongodb-latest.xsd b/src/main/resources/www.liquibase.org/xml/ns/mongodb/liquibase-mongodb-latest.xsd index 16b16850..4f43ede1 100644 --- a/src/main/resources/www.liquibase.org/xml/ns/mongodb/liquibase-mongodb-latest.xsd +++ b/src/main/resources/www.liquibase.org/xml/ns/mongodb/liquibase-mongodb-latest.xsd @@ -140,5 +140,20 @@ - + + + + + + + + + + + + + + + + diff --git a/src/test/java/liquibase/ext/mongodb/change/UpdateManyChangeTest.java b/src/test/java/liquibase/ext/mongodb/change/UpdateManyChangeTest.java new file mode 100644 index 00000000..81198053 --- /dev/null +++ b/src/test/java/liquibase/ext/mongodb/change/UpdateManyChangeTest.java @@ -0,0 +1,60 @@ +package liquibase.ext.mongodb.change; + +/*- + * #%L + * Liquibase MongoDB Extension + * %% + * Copyright (C) 2019 Mastercard + * %% + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import liquibase.ChecksumVersion; +import liquibase.changelog.ChangeSet; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static liquibase.ext.mongodb.TestUtils.getChangesets; +import static org.assertj.core.api.Assertions.assertThat; + +class UpdateManyChangeTest extends AbstractMongoChangeTest { + + @Test + void getConfirmationMessage() { + final UpdateManyChange updateManyChange = new UpdateManyChange(); + updateManyChange.setCollectionName("collection1"); + assertThat(updateManyChange.getConfirmationMessage()).isEqualTo("Documents updated in collection collection1"); + } + + @Test + @SneakyThrows + void generateStatements() { + final List changeSets = getChangesets("liquibase/ext/changelog.update-many.test.xml", database); + + assertThat(changeSets) + .hasSize(1).first() + .returns("9:221a9c901f6a318845c509ff231d3698", changeSet -> changeSet.generateCheckSum(ChecksumVersion.latest()).toString()); + + assertThat(changeSets.get(0).getChanges()) + .hasSize(1) + .hasOnlyElementsOfType(UpdateManyChange.class); + + assertThat(changeSets.get(0).getChanges().get(0)) + .hasFieldOrPropertyWithValue("collectionName", "updateManyTest1") + .hasFieldOrPropertyWithValue("filter", "{ name: \"first\" }") + .hasFieldOrPropertyWithValue("update", "{ $set: { name: \"modified\" } }"); + } +} diff --git a/src/test/java/liquibase/ext/mongodb/statement/UpdateManyStatementIT.java b/src/test/java/liquibase/ext/mongodb/statement/UpdateManyStatementIT.java new file mode 100644 index 00000000..c24b3553 --- /dev/null +++ b/src/test/java/liquibase/ext/mongodb/statement/UpdateManyStatementIT.java @@ -0,0 +1,117 @@ +package liquibase.ext.mongodb.statement; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.mongodb.client.FindIterable; + +import liquibase.ext.AbstractMongoIntegrationTest; +import static liquibase.ext.mongodb.TestUtils.COLLECTION_NAME_1; + +class UpdateManyStatementIT extends AbstractMongoIntegrationTest { + + private final Document first = new Document("name", "first"); + private final Document second = new Document("name", "second"); + private final Document modified = new Document("name", "modified"); + private final Document update = new Document("$set", modified); + private final List aggregation = Arrays.asList( + new Document("$set", + new Document("name", + new Document("$replaceAll", + new Document(Map.of( + "input", "$name", + "find", "first", + "replacement", "second")))))); + private final Document emptyDocument = new Document(); + + private String collectionName; + + @BeforeEach + public void createCollectionName() { + collectionName = COLLECTION_NAME_1 + System.nanoTime(); + } + + @Test + public void testUpdateWhenNoDocumentFound() { + int updated = new UpdateManyStatement(collectionName, emptyDocument, update) + .update(database); + assertThat(updated).isEqualTo(0); + } + + @Test + public void testUpdateWhenDocumentFound() { + + new InsertOneStatement(collectionName, first).execute(database); + + int updated = new UpdateManyStatement(collectionName, emptyDocument, update) + .update(database); + assertThat(updated).isEqualTo(1); + + final FindIterable docs = mongoDatabase.getCollection(collectionName).find(); + assertThat(docs).hasSize(1); + assertThat(docs.iterator().next()) + .containsEntry("name", "modified"); + } + + @Test + public void testUpdateWithMatchingFilter() { + + new InsertOneStatement(collectionName, first).execute(database); + new InsertOneStatement(collectionName, second).execute(database); + + int updated = new UpdateManyStatement(collectionName, second, update) + .update(database); + assertThat(updated).isEqualTo(1); + + final FindIterable docs = mongoDatabase.getCollection(collectionName).find(modified); + assertThat(docs).hasSize(1); + assertThat(docs.iterator().next()) + .containsEntry("name", "modified"); + } + + @Test + public void testUpdateWhenMultipleDocumentsFound() { + + new InsertOneStatement(collectionName, second).execute(database); + new InsertOneStatement(collectionName, second).execute(database); + + int updated = new UpdateManyStatement(collectionName, second, update) + .update(database); + assertThat(updated).isEqualTo(2); + + final FindIterable docs = mongoDatabase.getCollection(collectionName).find(modified); + assertThat(docs).hasSize(2); + assertThat(docs).allMatch(doc -> doc.getString("name").equals("modified")); + } + + @Test + public void testUpdateWithAggregation() { + + new InsertOneStatement(collectionName, first).execute(database); + + int updated = new UpdateManyStatement(collectionName, first, aggregation) + .update(database); + assertThat(updated).isEqualTo(1); + + final FindIterable docs = mongoDatabase.getCollection(collectionName).find(second); + assertThat(docs).hasSize(1); + assertThat(docs.iterator().next()) + .containsEntry("name", "second"); + } + + @Test + void toStringJs() { + final UpdateManyStatement statement = new UpdateManyStatement(COLLECTION_NAME_1, first, update); + assertThat(statement.toJs()) + .isEqualTo(statement.toString()) + .isEqualTo("db.collectionName.updateMany(" + + "{\"name\": \"first\"}, " + + "{\"$set\": {\"name\": \"modified\"}});"); + } +} diff --git a/src/test/resources/liquibase/ext/changelog.update-many.test.xml b/src/test/resources/liquibase/ext/changelog.update-many.test.xml new file mode 100644 index 00000000..cc4959e5 --- /dev/null +++ b/src/test/resources/liquibase/ext/changelog.update-many.test.xml @@ -0,0 +1,19 @@ + + + + + + { name: "first" } + + + { $set: { name: "modified" } } + + + + + \ No newline at end of file