Skip to content

Commit

Permalink
RunCommandStatement made generic when checked for failures
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandru-slobodcicov committed Feb 2, 2021
1 parent c90363b commit 43d400e
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
import lombok.Getter;
import org.bson.Document;

import java.util.List;

import static java.util.Objects.nonNull;

@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public abstract class AbstractRunCommandStatement extends AbstractNoSqlStatement
Expand All @@ -42,7 +46,7 @@ public abstract class AbstractRunCommandStatement extends AbstractNoSqlStatement

@Override
public void execute(final MongoConnection connection) {
Document response = run(connection);
final Document response = run(connection);
checkResponse(response);
}

Expand All @@ -51,17 +55,37 @@ public Document run(final MongoConnection connection) {
}

/**
* Check the response and throw an appropriate exception if the command was not successful
* Inspects response Document for any issues.
* For example the server responds with { "ok" : 1 } (success) even when run command fails to insert the document.
* The contents of the response is checked to see if the document was actually inserted
* For more information see the manual page
*
* @param responseDocument the response document
* @throws MongoException a MongoException to be thrown
* @see <a href="https://docs.mongodb.com/manual/reference/command/insert/#output">Insert Output</a>
* <p>
* Check the response and throw an appropriate exception if the command was not successful
*/
abstract void checkResponse(Document responseDocument) throws MongoException;
protected void checkResponse(final Document responseDocument) throws MongoException {
final List<Document> writeErrors = responseDocument.getList(BsonUtils.WRITE_ERRORS, Document.class);
if (nonNull(writeErrors) && !writeErrors.isEmpty()) {
throw new MongoException("Command failed. The full response is " + responseDocument.toJson());
}
}

@Override
public String getCommandName() {
return COMMAND_NAME;
}

/**
* Returns the RunCommand command name.
*
* @return the run command as this is not used and not required for a generic RunCommandStatement
* @see <a href="https://docs.mongodb.com/manual/reference/command/">Database Commands</a>
*/
public abstract String getRunCommandName();

@Override
public String toJs() {
return SHELL_DB_PREFIX
Expand All @@ -70,6 +94,4 @@ public String toJs() {
+ BsonUtils.toJson(command)
+ ");";
}


}
3 changes: 3 additions & 0 deletions src/main/java/liquibase/ext/mongodb/statement/BsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
@NoArgsConstructor(access = PRIVATE)
public final class BsonUtils {

public static final String WRITE_ERRORS = "writeErrors";
public static final String DOCUMENTS = "documents";

public static final DocumentCodec DOCUMENT_CODEC =
new DocumentCodec(fromProviders(
new UuidCodecProvider(UuidRepresentation.STANDARD),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* #L%
*/

import com.mongodb.MongoException;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.bson.Document;
Expand All @@ -44,14 +43,8 @@ public CreateCollectionStatement(final String collectionName, final Document opt
super(BsonUtils.toCommand(RUN_COMMAND_NAME, collectionName, options));
}

/**
* The server responds with { "ok": 0 } (failure) if this command fails
* Therefore the response document does not need to be explicitly checked.
* @param responseDocument the response document
* @throws MongoException - does not throw in this case
*/
@Override
void checkResponse(Document responseDocument) throws MongoException {
// NoOp
public String getRunCommandName() {
return RUN_COMMAND_NAME;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,10 @@
* #L%
*/

import com.mongodb.MongoException;
import com.mongodb.MongoWriteException;
import com.mongodb.WriteError;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.bson.BsonDocument;
import org.bson.Document;

import java.util.List;

import static java.util.Objects.nonNull;
import static java.util.Collections.singletonList;
import static liquibase.ext.mongodb.statement.BsonUtils.orEmptyDocument;
Expand All @@ -43,43 +37,32 @@
@EqualsAndHashCode(callSuper = true)
public class InsertOneStatement extends AbstractRunCommandStatement {

public static final String COMMAND_NAME = "insert";
public static final String RUN_COMMAND_NAME = "insert";

public InsertOneStatement(final String collectionName, final String document, final String options) {
this(collectionName, orEmptyDocument(document), orEmptyDocument(options));
}

public InsertOneStatement(final String collectionName, final Document document, final Document options) {
super(BsonUtils.toCommand(COMMAND_NAME, collectionName, combine(document, options)));
super(BsonUtils.toCommand(RUN_COMMAND_NAME, collectionName, combine(document, options)));
}

private static Document combine(final Document document, final Document options) {
Document combined = new Document("documents", singletonList(document));
final Document combined = new Document(BsonUtils.DOCUMENTS, singletonList(document));
if (nonNull(options)) {
combined.putAll(options);
}
return combined;
}

/**
* The server responds with { "ok" : 1 } (success) even when this command fails to insert the document.
* The contents of the response is checked to see if the document was actually inserted
* For more information see the manual page: https://docs.mongodb.com/manual/reference/command/insert/#output
* Returns the RunCommand command name.
*
* @param responseDocument the response document
* @throws MongoWriteException containing the code and error message if the document failed to insert
* @return the run command as this is not used and not required for a generic RunCommandStatement
* @see <a href="https://docs.mongodb.com/manual/reference/command/">Database Commands</a>
*/
@Override
public void checkResponse(Document responseDocument) throws MongoException {
if(responseDocument.getInteger("n")==1) return;
List<Document> writeErrors = responseDocument.getList("writeErrors", Document.class);
if(writeErrors.size()==1) {
Document firstError = writeErrors.get(0);
int code = firstError.getInteger("code");
String message = firstError.getString("errmsg");
WriteError error = new WriteError(code, message, new BsonDocument());
throw new MongoWriteException(error, null);
}
public String getRunCommandName() {
return RUN_COMMAND_NAME;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* #L%
*/

import com.mongodb.MongoException;
import lombok.EqualsAndHashCode;
import org.bson.Document;

Expand All @@ -36,15 +35,11 @@ public RunCommandStatement(final Document command) {
}

/**
* Responses are not checked for adhoc commands.
* This could result in unexpected behaviour
* TODO: Minimally check if { "ok": 0 } and throw an exception containing the responseDocument
*
* @param responseDocument the response document
* @throws MongoException does not throw in this case
* Returns the RunCommand command name
* @return null as this is not used and not required for a generic RunCommandStatement
*/
@Override
void checkResponse(Document responseDocument) throws MongoException {
// NoOp
public String getRunCommandName() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package liquibase.ext.mongodb.changelog;

import com.mongodb.MongoWriteException;
import com.mongodb.MongoException;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Filters;
import liquibase.ext.AbstractMongoIntegrationTest;
import liquibase.ext.mongodb.statement.FindAllStatement;
import liquibase.ext.mongodb.statement.InsertOneStatement;
import lombok.SneakyThrows;
import org.bson.Document;
import org.junit.jupiter.api.Test;

Expand All @@ -27,6 +28,7 @@ class AdjustChangeLogCollectionStatementIT extends AbstractMongoIntegrationTest
protected FindAllStatement findAllStatement = new FindAllStatement(LOG_COLLECTION_NAME);

@Test
@SneakyThrows
void executeToJSTest() {
AdjustChangeLogCollectionStatement adjustChangeLogCollectionStatement =
new AdjustChangeLogCollectionStatement(LOG_COLLECTION_NAME);
Expand Down Expand Up @@ -69,6 +71,7 @@ void executeToJSTest() {
}

@Test
@SneakyThrows
void executeTest() {

new CreateChangeLogCollectionStatement(LOG_COLLECTION_NAME).execute(connection);
Expand Down Expand Up @@ -129,6 +132,7 @@ void executeTest() {
}

@Test
@SneakyThrows
void insertDataTest() {
// Returns empty even when collection does not exists
assertThat(findAllStatement.queryForList(connection)).isEmpty();
Expand All @@ -142,21 +146,24 @@ void insertDataTest() {
final Document options = new Document();
final Document minimal = new Document()
.append("id", "cs1");
assertThatExceptionOfType(MongoWriteException.class)
assertThatExceptionOfType(MongoException.class)
.isThrownBy(() -> new InsertOneStatement(LOG_COLLECTION_NAME, minimal, options).execute(connection))
.withMessageStartingWith("Document failed validation");
.withMessageStartingWith("Command failed. The full response is")
.withMessageContaining("Document failed validation");

// Minimal not all required fields
minimal.append("author", "Alex");
assertThatExceptionOfType(MongoWriteException.class)
assertThatExceptionOfType(MongoException.class)
.isThrownBy(() -> new InsertOneStatement(LOG_COLLECTION_NAME, minimal, options).execute(connection))
.withMessageStartingWith("Document failed validation");
.withMessageStartingWith("Command failed. The full response is")
.withMessageContaining("Document failed validation");

// Minimal not all required fields
minimal.append("fileName", "liquibase/file.xml");
assertThatExceptionOfType(MongoWriteException.class)
assertThatExceptionOfType(MongoException.class)
.isThrownBy(() -> new InsertOneStatement(LOG_COLLECTION_NAME, minimal, options).execute(connection))
.withMessageStartingWith("Document failed validation");
.withMessageStartingWith("Command failed. The full response is")
.withMessageContaining("Document failed validation");

// Minimal accepted
minimal.append("execType", "EXECUTED");
Expand All @@ -171,9 +178,10 @@ void insertDataTest() {

// Unique constraint failure
minimal.remove("_id");
assertThatExceptionOfType(MongoWriteException.class)
assertThatExceptionOfType(MongoException.class)
.isThrownBy(() -> new InsertOneStatement(LOG_COLLECTION_NAME, minimal, options).execute(connection))
.withMessageStartingWith("E11000 duplicate key error collection");
.withMessageStartingWith("Command failed. The full response is")
.withMessageContaining("E11000 duplicate key error collection");

// Extra fields are allowed
minimal.remove("_id");
Expand All @@ -187,9 +195,10 @@ void insertDataTest() {
// Nulls fail validation
minimal.remove("_id");
minimal.append("id", "cs3").append("fileName", null);
assertThatExceptionOfType(MongoWriteException.class)
assertThatExceptionOfType(MongoException.class)
.isThrownBy(() -> new InsertOneStatement(LOG_COLLECTION_NAME, minimal, options).execute(connection))
.withMessageStartingWith("Document failed validation");
.withMessageStartingWith("Command failed. The full response is")
.withMessageContaining("Document failed validation");

// Maximum
final Date dateExecuted = new Date();
Expand Down Expand Up @@ -261,6 +270,7 @@ void insertDataTest() {
}

@Test
@SneakyThrows
void insertDataNoValidatorTest() {
// Returns empty even when collection does not exists
assertThat(findAllStatement.queryForList(connection)).isEmpty();
Expand Down

0 comments on commit 43d400e

Please sign in to comment.