diff --git a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java index f6257e88..dbc6f5a7 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java @@ -16,22 +16,18 @@ package com.mongodb.hibernate.jdbc; -import static com.mongodb.hibernate.internal.MongoConstants.EXTENDED_JSON_WRITER_SETTINGS; import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static com.mongodb.hibernate.jdbc.MongoStatementIntegrationTests.doWorkWithSpecifiedAutoCommit; import static com.mongodb.hibernate.jdbc.MongoStatementIntegrationTests.insertTestData; -import static java.lang.String.format; import static java.sql.Statement.SUCCESS_NO_INFO; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.params.provider.Arguments.of; import com.mongodb.client.MongoCollection; import com.mongodb.client.model.Sorts; @@ -42,14 +38,10 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLSyntaxErrorException; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.function.Function; -import java.util.stream.Stream; -import org.bson.BSONException; import org.bson.BsonDocument; import org.hibernate.Session; import org.hibernate.SessionFactory; @@ -64,9 +56,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.Parameter; import org.junit.jupiter.params.ParameterizedClass; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; @ExtendWith(MongoExtension.class) @@ -243,111 +232,6 @@ private void assertRoundTrip(SqlConsumer executor) { }); } - @Test - void testNoCommandNameProvidedExecuteQuery() { - assertInvalidMql( - """ - {}""", - PreparedStatement::executeQuery, - "Invalid MQL. Command name is missing: [{}]"); - } - - @Test - void testNoCommandNameProvidedExecuteUpdate() { - assertInvalidMql( - """ - {}""", - PreparedStatement::executeUpdate, - "Invalid MQL. Command name is missing: [{}]"); - } - - @Test - void testNoCommandNameProvidedExecuteBatch() { - assertInvalidMql( - """ - {}""", - preparedStatement -> { - preparedStatement.addBatch(); - preparedStatement.executeBatch(); - }, - "Invalid MQL. Command name is missing: [{}]"); - } - - @Test - void testNoCollectionNameProvidedExecuteQuery() { - assertInvalidMql( - """ - { - aggregate: {} - }""", - PreparedStatement::executeQuery, - """ - Invalid MQL. Collection name is missing [{"aggregate": {}}]"""); - } - - @Test - void testNoCollectionNameProvidedExecuteUpdate() { - assertInvalidMql( - """ - { - insert: {} - }""", - PreparedStatement::executeUpdate, - """ - Invalid MQL. Collection name is missing [{"insert": {}}]"""); - } - - @Test - void testNoCollectionNameProvidedExecuteBatch() { - assertInvalidMql( - """ - { - insert: {} - }""", - preparedStatement -> { - preparedStatement.addBatch(); - preparedStatement.executeBatch(); - }, - """ - Invalid MQL. Collection name is missing [{"insert": {}}]"""); - } - - @Test - void testAbsentRequiredAggregateCommandField() { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - aggregate: "books" - }"""; - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeQuery) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL: [%s]".formatted(toExtendedJson(mql))) - .cause() - .isInstanceOf(BSONException.class) - .hasMessage("Document does not contain key pipeline"); - } - }); - } - - @Test - void testAbsentRequiredProjectAggregationPipelineStage() { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - aggregate: "books", - "pipeline": [] - }"""; - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeQuery) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL. $project stage is missing [%s]".formatted(toExtendedJson(mql))); - } - }); - } - @Nested class ExecuteBatchTests { private static final String INSERT_MQL = @@ -398,8 +282,7 @@ void testQueriesReturningResult() { void testEmptyBatch() { doWorkAwareOfAutoCommit(connection -> { try (var pstmt = connection.prepareStatement(INSERT_MQL)) { - var updateCounts = pstmt.executeBatch(); - assertEquals(0, updateCounts.length); + assertExecuteBatch(pstmt, 0); } }); @@ -408,7 +291,7 @@ void testEmptyBatch() { @Test @DisplayName("Test statement’s batch of commands is reset once executeBatch returns") - void testBatchQueueIsResetAfterExecute() { + void testBatchOfCommandsIsResetAfterExecute() { doWorkAwareOfAutoCommit(connection -> { try (var pstmt = connection.prepareStatement( """ @@ -498,7 +381,7 @@ void testBatchInsert() { }] }""")) { - for (int i = 1; i <= batchSize; i++) { + for (var i = 1; i <= batchSize; i++) { pstmt.setInt(1, i); pstmt.setString(2, "Book " + i); pstmt.addBatch(); @@ -508,14 +391,14 @@ void testBatchInsert() { }); var expectedDocs = new ArrayList(); - for (int i = 1; i <= batchSize; i++) { - expectedDocs.add(BsonDocument.parse(format( + for (var i = 1; i <= batchSize; i++) { + expectedDocs.add(BsonDocument.parse( """ { "_id": %d, "title": "Book %d" - }""", - i, i))); + }""" + .formatted(i, i))); } assertThat(mongoCollection.find()).containsExactlyElementsOf(expectedDocs); } @@ -535,7 +418,7 @@ void testBatchUpdate() { multi: true }] }""")) { - for (int i = 1; i <= batchSize; i++) { + for (var i = 1; i <= batchSize; i++) { pstmt.setInt(1, i); pstmt.setString(2, "Book " + i); pstmt.addBatch(); @@ -545,14 +428,14 @@ void testBatchUpdate() { }); var expectedDocs = new ArrayList(); - for (int i = 1; i <= batchSize; i++) { - expectedDocs.add(BsonDocument.parse(format( + for (var i = 1; i <= batchSize; i++) { + expectedDocs.add(BsonDocument.parse( """ { "_id": %d, "title": "Book %d" - }""", - i, i))); + }""" + .formatted(i, i))); } assertThat(mongoCollection.find()).containsExactlyElementsOf(expectedDocs); } @@ -571,7 +454,7 @@ void testBatchDelete() { limit: 0 }] }""")) { - for (int i = 1; i <= batchSize; i++) { + for (var i = 1; i <= batchSize; i++) { pstmt.setInt(1, i); pstmt.addBatch(); } @@ -584,9 +467,9 @@ void testBatchDelete() { private static void assertExecuteBatch(PreparedStatement pstmt, int expectedUpdateCountsSize) throws SQLException { - int[] updateCounts = pstmt.executeBatch(); + var updateCounts = pstmt.executeBatch(); assertEquals(expectedUpdateCountsSize, updateCounts.length); - for (int updateCount : updateCounts) { + for (var updateCount : updateCounts) { assertEquals(SUCCESS_NO_INFO, updateCount); } } @@ -766,282 +649,6 @@ void testDelete() { }"""))); } - @ParameterizedTest(name = "test not supported commands. Parameters: {0}") - @ValueSource(strings = {"findAndModify", "aggregate", "bulkWrite"}) - void testNotSupportedCommands(String commandName) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement( - """ - { - %s: "books" - }""" - .formatted(commandName))) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLFeatureNotSupportedException.class) - .hasMessageContaining(commandName); - } - }); - } - - @ParameterizedTest(name = "test not supported update command field. Parameters: option={0}") - @ValueSource( - strings = { - "maxTimeMS: 1", - "writeConcern: {}", - "bypassDocumentValidation: true", - "comment: {}", - "ordered: true", - "let: {}" - }) - void testNotSupportedUpdateCommandField(String unsupportedField) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement( - """ - { - update: "books", - updates: [ - { - q: { author: { $eq: "Leo Tolstoy" } }, - u: { $set: { outOfStock: true } }, - multi: true - } - ], - %s - }""" - .formatted(unsupportedField))) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLFeatureNotSupportedException.class) - .hasMessage("Unsupported field in [update] command: [%s]" - .formatted(getFieldName(unsupportedField))); - } - }); - } - - @Test - void testAbsentRequiredUpdateCommandField() { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - update: "books" - }"""; - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL: [%s]".formatted(toExtendedJson(mql))) - .cause() - .hasMessage("Document does not contain key updates"); - } - }); - } - - @ParameterizedTest(name = "test not supported delete command field. Parameters: option={0}") - @ValueSource(strings = {"maxTimeMS: 1", "writeConcern: {}", "comment: {}", "ordered: true", "let: {}"}) - void testNotSupportedDeleteCommandField(String unsupportedField) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement( - """ - { - delete: "books", - deletes: [ - { - q: { author: { $eq: "Leo Tolstoy" } }, - limit: 0 - } - ] - %s - }""" - .formatted(unsupportedField))) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLFeatureNotSupportedException.class) - .hasMessage("Unsupported field in [delete] command: [%s]" - .formatted(getFieldName(unsupportedField))); - } - }); - } - - @Test - void testAbsentRequiredDeleteCommandField() { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - delete: "books" - }"""; - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL: [%s]".formatted(toExtendedJson(mql))) - .cause() - .hasMessage("Document does not contain key deletes"); - } - }); - } - - @ParameterizedTest(name = "test not supported insert command field. Parameters: option={0}") - @ValueSource( - strings = { - "maxTimeMS: 1", - "writeConcern: {}", - "bypassDocumentValidation: true", - "comment: {}", - "ordered: true", - "let: {}" - }) - void testNotSupportedInsertCommandField(String unsupportedField) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement( - """ - { - insert: "books", - documents: [ - { - _id: 1 - } - ], - %s - }""" - .formatted(unsupportedField))) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLFeatureNotSupportedException.class) - .hasMessage("Unsupported field in [insert] command: [%s]" - .formatted(getFieldName(unsupportedField))); - } - }); - } - - @Test - void testAbsentRequiredInsertCommandField() { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - insert: "books" - }"""; - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL: [%s]".formatted(toExtendedJson(mql))) - .cause() - .hasMessage("Document does not contain key documents"); - } - }); - } - - private static Stream unsupportedUpdateStatementFields() { - return Stream.of( - of("hint: {}", "Unsupported field in [update] statement: [hint]"), - of("hint: \"a\"", "Unsupported field in [update] statement: [hint]"), - of("collation: {}", "Unsupported field in [update] statement: [collation]"), - of("arrayFilters: []", "Unsupported field in [update] statement: [arrayFilters]"), - of("sort: {}", "Unsupported field in [update] statement: [sort]"), - of("upsert: true", "Unsupported field in [update] statement: [upsert]"), - of("u: []", "Only document type is supported as value for field: [u]"), - of("c: {}", "Unsupported field in [update] statement: [c]")); - } - - @ParameterizedTest(name = "test not supported update statement field. Parameters: option={0}") - @MethodSource("unsupportedUpdateStatementFields") - void testNotSupportedUpdateStatemenField(String unsupportedField, String expectedMessage) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement( - """ - { - update: "books", - updates: [ - { - q: { author: { $eq: "Leo Tolstoy" } }, - u: { $set: { outOfStock: true } }, - multi: true, - %s - } - ] - }""" - .formatted(unsupportedField))) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLFeatureNotSupportedException.class) - .hasMessage(expectedMessage); - } - }); - } - - @ParameterizedTest(name = "test absent required update statement field. Parameters: fieldToRemove={0}") - @ValueSource(strings = {"q: {}", "u: {}"}) - void testAbsentRequiredUpdateStatementField(String fieldToRemove) { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - update: "books", - updates: [ - { - q: {}, - u: {}, - } - ] - }""" - .replace(fieldToRemove + ",", ""); - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL: [%s]".formatted(toExtendedJson(mql))) - .cause() - .hasMessage("Document does not contain key %s".formatted(getFieldName(fieldToRemove))); - } - }); - } - - @ParameterizedTest(name = "test not supported delete statement field. Parameters: option={0}") - @ValueSource(strings = {"hint: {}", "hint: \"a\"", "collation: {}"}) - void testNotSupportedDeleteStatementField(String unsupportedField) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement( - """ - { - delete: "books", - deletes: [ - { - q: { author: { $eq: "Leo Tolstoy" } }, - limit: 0, - %s - } - ] - }""" - .formatted(unsupportedField))) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLFeatureNotSupportedException.class) - .hasMessage("Unsupported field in [delete] statement: [%s]" - .formatted(getFieldName(unsupportedField))); - } - }); - } - - @ParameterizedTest(name = "test absent required update statement field. Parameters: fieldToRemove={0}") - @ValueSource(strings = {"q: {}", "limit: 0"}) - void testAbsentRequiredDeleteStatementField(String fieldToRemove) { - doWorkAwareOfAutoCommit(connection -> { - String mql = - """ - { - delete: "books", - deletes: [ - { - q: {}, - limit: 0, - } - ] - }""" - .replace(fieldToRemove + ",", ""); - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(pstm::executeUpdate) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage("Invalid MQL: [%s]".formatted(toExtendedJson(mql))) - .cause() - .hasMessage("Document does not contain key %s".formatted(getFieldName(fieldToRemove))); - } - }); - } - private void assertExecuteUpdate( Function pstmtProvider, int expectedUpdatedRowCount, @@ -1056,30 +663,11 @@ private void assertExecuteUpdate( } } - private void assertInvalidMql( - String mql, SqlConsumer executor, String expectedExceptionMessage) { - doWorkAwareOfAutoCommit(connection -> { - try (PreparedStatement pstm = connection.prepareStatement(mql)) { - assertThatThrownBy(() -> executor.accept(pstm)) - .isInstanceOf(SQLSyntaxErrorException.class) - .hasMessage(expectedExceptionMessage); - } - }); - } - private void doWorkAwareOfAutoCommit(Work work) { doWorkWithSpecifiedAutoCommit(autoCommit, session, work); } - private static String getFieldName(String unsupportedField) { - return BsonDocument.parse("{" + unsupportedField + "}").getFirstKey(); - } - - private String toExtendedJson(String mql) { - return BsonDocument.parse(mql).toJson(EXTENDED_JSON_WRITER_SETTINGS); - } - - interface SqlConsumer { + private interface SqlConsumer { void accept(T t) throws SQLException; } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java index 54bba000..49af6107 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -377,7 +377,7 @@ void testQueryOptionsSetFirstResultAndMaxResults() { @Nested class WithHqlClauses { - private static final String expectedMqlTemplate = + private static final String EXPECTED_MQL_TEMPLATE = """ { "aggregate": "books", @@ -415,7 +415,7 @@ void testFirstResultConflictingOnly() { q.setParameter("limit", 10) .setParameter("offset", 0) .setFirstResult(firstResult), - expectedMqlTemplate.formatted("{\"$skip\": " + firstResult + "}"), + EXPECTED_MQL_TEMPLATE.formatted("{\"$skip\": " + firstResult + "}"), expectedBooks, Set.of(Book.COLLECTION_NAME)); } @@ -432,7 +432,7 @@ void testMaxResultsConflictingOnly() { q.setParameter("limit", 10) .setParameter("offset", 0) .setMaxResults(maxResults), - expectedMqlTemplate.formatted("{\"$limit\": " + maxResults + "}"), + EXPECTED_MQL_TEMPLATE.formatted("{\"$limit\": " + maxResults + "}"), expectedBooks, Set.of(Book.COLLECTION_NAME)); } @@ -451,7 +451,7 @@ void testBothFirstResultAndMaxResultsConflicting() { .setParameter("offset", 0) .setFirstResult(firstResult) .setMaxResults(maxResults), - expectedMqlTemplate.formatted( + EXPECTED_MQL_TEMPLATE.formatted( "{\"$skip\": " + firstResult + "}," + "{\"$limit\": " + maxResults + "}"), expectedBooks, Set.of(Book.COLLECTION_NAME)); @@ -497,7 +497,7 @@ void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { class QueryPlanCacheTests extends AbstractQueryIntegrationTests { private static final String HQL = "from Book order by id"; - private static final String expectedMqlTemplate = + private static final String EXPECTED_MQL_TEMPLATE = """ { "aggregate": "books", @@ -544,7 +544,7 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null, format( - expectedMqlTemplate, + EXPECTED_MQL_TEMPLATE, (isFirstResultSet ? "{\"$skip\": 5}," : ""), (isMaxResultsSet ? "{\"$limit\": 10}," : ""))); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCount(); @@ -556,7 +556,7 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null, format( - expectedMqlTemplate, + EXPECTED_MQL_TEMPLATE, (isFirstResultSet ? "{\"$skip\": 3}," : ""), (isMaxResultsSet ? "{\"$limit\": 6}," : ""))); assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) @@ -567,16 +567,16 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS @Test void testCacheInvalidatedDueToQueryOptionsAdded() { getSessionFactoryScope().inTransaction(session -> { - setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); + setQueryOptionsAndQuery(session, null, null, format(EXPECTED_MQL_TEMPLATE, "", "")); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCount(); assertThat(initialSelectTranslatingCount).isPositive(); - setQueryOptionsAndQuery(session, 1, null, format(expectedMqlTemplate, "{\"$skip\": 1},", "")); + setQueryOptionsAndQuery(session, 1, null, format(EXPECTED_MQL_TEMPLATE, "{\"$skip\": 1},", "")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 1); setQueryOptionsAndQuery( - session, 1, 5, format(expectedMqlTemplate, "{\"$skip\": 1},", "{\"$limit\": 5},")); + session, 1, 5, format(EXPECTED_MQL_TEMPLATE, "{\"$skip\": 1},", "{\"$limit\": 5},")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 2); }); @@ -586,15 +586,15 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { void testCacheInvalidatedDueToQueryOptionsRemoved() { getSessionFactoryScope().inTransaction(session -> { setQueryOptionsAndQuery( - session, 10, 5, format(expectedMqlTemplate, "{\"$skip\": 10},", "{\"$limit\": 5},")); + session, 10, 5, format(EXPECTED_MQL_TEMPLATE, "{\"$skip\": 10},", "{\"$limit\": 5},")); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCount(); assertThat(initialSelectTranslatingCount).isPositive(); - setQueryOptionsAndQuery(session, null, 5, format(expectedMqlTemplate, "", "{\"$limit\": 5},")); + setQueryOptionsAndQuery(session, null, 5, format(EXPECTED_MQL_TEMPLATE, "", "{\"$limit\": 5},")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 1); - setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); + setQueryOptionsAndQuery(session, null, null, format(EXPECTED_MQL_TEMPLATE, "", "")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 2); }); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index 754f6fdf..e34a0e35 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -115,10 +115,10 @@ ResultSet executeQuery(BsonDocument command) throws SQLException { var pipeline = command.getArray("pipeline").stream() .map(BsonValue::asDocument) .toList(); - var projectStageIndex = pipeline.size() - 1; if (pipeline.isEmpty()) { throw createSyntaxErrorException("%s. $project stage is missing [%s]", command, null); } + var projectStageIndex = pipeline.size() - 1; var fieldNames = getFieldNamesFromProjectStage( pipeline.get(projectStageIndex).getDocument("$project")); startTransactionIfNeeded(); diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommandTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommandTests.java index d812ae51..a9b03519 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommandTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstUpdateCommandTests.java @@ -23,7 +23,6 @@ import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFieldOperationFilter; -import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter; import java.util.List; import org.bson.BsonInt64; import org.bson.BsonString; @@ -38,15 +37,14 @@ void testRendering() { var astFieldUpdate1 = new AstFieldUpdate("title", new AstLiteral(new BsonString("War and Peace"))); var astFieldUpdate2 = new AstFieldUpdate("author", new AstLiteral(new BsonString("Leo Tolstoy"))); - final AstFilter filter; - filter = new AstFieldOperationFilter( + var filter = new AstFieldOperationFilter( "_id", new AstComparisonFilterOperation( AstComparisonFilterOperator.EQ, new AstLiteral(new BsonInt64(12345L)))); var updateCommand = new AstUpdateCommand(collection, filter, List.of(astFieldUpdate1, astFieldUpdate2)); - final String expectedJson = + var expectedJson = """ {"update": "books", "updates": [{"q": {"_id": {"$eq": {"$numberLong": "12345"}}}, "u": {"$set": {"title": "War and Peace", "author": "Leo Tolstoy"}}, "multi": true}]}\ """; diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 9ca43125..7eb24c71 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -16,12 +16,15 @@ package com.mongodb.hibernate.jdbc; +import static com.mongodb.hibernate.internal.MongoConstants.EXTENDED_JSON_WRITER_SETTINGS; +import static com.mongodb.hibernate.jdbc.MongoStatement.NO_ERROR_CODE; import static java.sql.Statement.SUCCESS_NO_INFO; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatObject; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -29,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Named.named; +import static org.junit.jupiter.params.provider.Arguments.of; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -46,7 +50,6 @@ import com.mongodb.MongoTimeoutException; import com.mongodb.ServerAddress; import com.mongodb.bulk.BulkWriteError; -import com.mongodb.bulk.BulkWriteInsert; import com.mongodb.bulk.BulkWriteResult; import com.mongodb.bulk.WriteConcernError; import com.mongodb.client.AggregateIterable; @@ -62,6 +65,7 @@ import java.sql.BatchUpdateException; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLSyntaxErrorException; import java.sql.Types; import java.util.Calendar; @@ -69,6 +73,7 @@ import java.util.function.Consumer; import java.util.stream.Stream; import org.assertj.core.api.ThrowingConsumer; +import org.bson.BSONException; import org.bson.BsonArray; import org.bson.BsonBoolean; import org.bson.BsonDocument; @@ -86,6 +91,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -140,7 +146,7 @@ class ParameterValueSettingTests { @Test @DisplayName("Happy path when all parameters are provided values") void testSuccess() throws SQLException { - BulkWriteResult bulkWriteResult = Mockito.mock(BulkWriteResult.class); + var bulkWriteResult = Mockito.mock(BulkWriteResult.class); doReturn(mongoCollection).when(mongoDatabase).getCollection(anyString(), eq(BsonDocument.class)); doReturn(bulkWriteResult).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); @@ -171,19 +177,364 @@ void testSuccess() throws SQLException { new BsonArray( List.of(new BsonString("array element"), new BsonObjectId(new ObjectId(1, 2))))) .append("objectId", new BsonObjectId(new ObjectId(2, 0))); - assertInstanceOf(InsertOneModel.class, writeModels.get(0)); - assertEquals(expectedDoc, ((InsertOneModel) writeModels.get(0)).getDocument()); + var insertOneModel = assertInstanceOf(InsertOneModel.class, writeModels.get(0)); + assertEquals(expectedDoc, insertOneModel.getDocument()); } } } + @Nested + class ExecuteInvalidOrUnsupportedMql { + @Test + void testNoCommandNameProvidedExecuteQuery() throws SQLException { + try (var pstm = createMongoPreparedStatement("{}")) { + assertThatThrownBy(pstm::executeQuery) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL. Command name is missing: [{}]"); + } + } + + @Test + void testNoCommandNameProvidedExecuteUpdate() throws SQLException { + try (var pstm = createMongoPreparedStatement("{}")) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL. Command name is missing: [{}]"); + } + } + + @Test + void testNoCommandNameProvidedExecuteBatch() throws SQLException { + try (var pstm = createMongoPreparedStatement("{}")) { + assertThatThrownBy(() -> { + pstm.addBatch(); + pstm.executeBatch(); + }) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL. Command name is missing: [{}]"); + } + } + + @Test + void testNoCollectionNameProvidedExecuteQuery() throws SQLException { + try (var pstm = createMongoPreparedStatement("{aggregate: {}}")) { + assertThatThrownBy(pstm::executeQuery) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage( + """ + Invalid MQL. Collection name is missing [{"aggregate": {}}]"""); + } + } + + @Test + void testNoCollectionNameProvidedExecuteUpdate() throws SQLException { + try (var pstm = createMongoPreparedStatement("{insert: {}}")) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage( + """ + Invalid MQL. Collection name is missing [{"insert": {}}]"""); + } + } + + @Test + void testNoCollectionNameProvidedExecuteBatch() throws SQLException { + try (var pstm = createMongoPreparedStatement("{insert: {}}")) { + assertThatThrownBy(() -> { + pstm.addBatch(); + pstm.executeBatch(); + }) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage( + """ + Invalid MQL. Collection name is missing [{"insert": {}}]"""); + } + } + + @Test + void testMissingRequiredAggregateCommandField() throws SQLException { + var mql = """ + {"aggregate": "books"}"""; + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeQuery) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL: [%s]".formatted(mql)) + .cause() + .isInstanceOf(BSONException.class) + .hasMessage("Document does not contain key pipeline"); + } + } + + @Test + void testMissingRequiredProjectAggregationPipelineStage() throws SQLException { + var mql = """ + {"aggregate": "books", "pipeline": []}"""; + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeQuery) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL. $project stage is missing [%s]".formatted(mql)); + } + } + + @ParameterizedTest(name = "test not supported command {0}") + @ValueSource(strings = {"findAndModify", "aggregate", "bulkWrite"}) + void testNotSupportedCommands(String commandName) throws SQLException { + try (var pstm = createMongoPreparedStatement( + """ + { + %s: "books" + }""" + .formatted(commandName))) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessageContaining(commandName); + } + } + + @ParameterizedTest(name = "test not supported update command field {0}") + @ValueSource( + strings = { + "maxTimeMS: 1", + "writeConcern: {}", + "bypassDocumentValidation: true", + "comment: {}", + "ordered: true", + "let: {}" + }) + void testNotSupportedUpdateCommandField(String unsupportedField) throws SQLException { + try (var pstm = createMongoPreparedStatement( + """ + { + update: "books", + updates: [ + { + q: { author: { $eq: "Leo Tolstoy" } }, + u: { $set: { outOfStock: true } }, + multi: true + } + ], + %s + }""" + .formatted(unsupportedField))) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Unsupported field in [update] command: [%s]" + .formatted(getFieldName(unsupportedField))); + } + } + + @Test + void testMissingRequiredUpdateCommandField() throws SQLException { + var mql = """ + {"update": "books"}"""; + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL: [%s]".formatted(mql)) + .cause() + .hasMessage("Document does not contain key updates"); + } + } + + @ParameterizedTest(name = "test not supported delete command field {0}") + @ValueSource(strings = {"maxTimeMS: 1", "writeConcern: {}", "comment: {}", "ordered: true", "let: {}"}) + void testNotSupportedDeleteCommandField(String unsupportedField) throws SQLException { + try (var pstm = createMongoPreparedStatement( + """ + { + delete: "books", + deletes: [ + { + q: { author: { $eq: "Leo Tolstoy" } }, + limit: 0 + } + ] + %s + }""" + .formatted(unsupportedField))) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Unsupported field in [delete] command: [%s]" + .formatted(getFieldName(unsupportedField))); + } + } + + @Test + void testMissingRequiredDeleteCommandField() throws SQLException { + var mql = """ + {"delete": "books"}"""; + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL: [%s]".formatted(mql)) + .cause() + .hasMessage("Document does not contain key deletes"); + } + } + + @ParameterizedTest(name = "test not supported insert command field {0}") + @ValueSource( + strings = { + "maxTimeMS: 1", + "writeConcern: {}", + "bypassDocumentValidation: true", + "comment: {}", + "ordered: true", + "let: {}" + }) + void testNotSupportedInsertCommandField(String unsupportedField) throws SQLException { + try (var pstm = createMongoPreparedStatement( + """ + { + insert: "books", + documents: [ + { + _id: 1 + } + ], + %s + }""" + .formatted(unsupportedField))) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Unsupported field in [insert] command: [%s]" + .formatted(getFieldName(unsupportedField))); + } + } + + @Test + void testMissingRequiredInsertCommandField() throws SQLException { + var mql = """ + {"insert": "books"}"""; + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL: [%s]".formatted(mql)) + .cause() + .hasMessage("Document does not contain key documents"); + } + } + + private static Stream testNotSupportedUpdateStatemenField() { + return Stream.of( + of("hint: {}", "Unsupported field in [update] statement: [hint]"), + of("hint: \"a\"", "Unsupported field in [update] statement: [hint]"), + of("collation: {}", "Unsupported field in [update] statement: [collation]"), + of("arrayFilters: []", "Unsupported field in [update] statement: [arrayFilters]"), + of("sort: {}", "Unsupported field in [update] statement: [sort]"), + of("upsert: true", "Unsupported field in [update] statement: [upsert]"), + of("u: []", "Only document type is supported as value for field: [u]"), + of("c: {}", "Unsupported field in [update] statement: [c]")); + } + + @ParameterizedTest(name = "test not supported update statement field {0}") + @MethodSource + void testNotSupportedUpdateStatemenField(String unsupportedField, String expectedMessage) throws SQLException { + try (var pstm = createMongoPreparedStatement( + """ + { + update: "books", + updates: [ + { + q: { author: { $eq: "Leo Tolstoy" } }, + u: { $set: { outOfStock: true } }, + multi: true, + %s + } + ] + }""" + .formatted(unsupportedField))) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage(expectedMessage); + } + } + + @ParameterizedTest(name = "test missing required update statement field {0}") + @ValueSource(strings = {"q", "u"}) + void testMissingRequiredUpdateStatementField(String missingFieldName) throws SQLException { + var mqlDocument = BsonDocument.parse( + """ + { + update: "books", + updates: [ + { + q: {}, + u: {}, + } + ] + }"""); + mqlDocument.getArray("updates").get(0).asDocument().remove(missingFieldName); + var mql = mqlDocument.toJson(EXTENDED_JSON_WRITER_SETTINGS); + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL: [%s]".formatted(mql)) + .cause() + .hasMessage("Document does not contain key %s".formatted(missingFieldName)); + } + } + + @ParameterizedTest(name = "test not supported delete statement field {0}") + @ValueSource(strings = {"hint: {}", "hint: \"a\"", "collation: {}"}) + void testNotSupportedDeleteStatementField(String unsupportedField) throws SQLException { + try (var pstm = createMongoPreparedStatement( + """ + { + delete: "books", + deletes: [ + { + q: { author: { $eq: "Leo Tolstoy" } }, + limit: 0, + %s + } + ] + }""" + .formatted(unsupportedField))) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLFeatureNotSupportedException.class) + .hasMessage("Unsupported field in [delete] statement: [%s]" + .formatted(getFieldName(unsupportedField))); + } + } + + @ParameterizedTest(name = "test missing required update statement field {0}") + @ValueSource(strings = {"q", "limit"}) + void testMissingRequiredDeleteStatementField(String missingFieldName) throws SQLException { + var mqlDocument = BsonDocument.parse( + """ + { + "delete": "books", + "deletes": [ + { + "q": {}, + "limit": {"$numberInt": "0"}, + } + ] + }"""); + mqlDocument.getArray("deletes").get(0).asDocument().remove(missingFieldName); + var mql = mqlDocument.toJson(EXTENDED_JSON_WRITER_SETTINGS); + try (var pstm = createMongoPreparedStatement(mql)) { + assertThatThrownBy(pstm::executeUpdate) + .isInstanceOf(SQLSyntaxErrorException.class) + .hasMessage("Invalid MQL: [%s]".formatted(mql)) + .cause() + .hasMessage("Document does not contain key %s".formatted(missingFieldName)); + } + } + + private static String getFieldName(String field) { + return BsonDocument.parse("{" + field + "}").getFirstKey(); + } + } + @Nested class ExecuteThrowsSqlExceptionTests { private static final String DUMMY_EXCEPTION_MESSAGE = "Test message"; private static final ServerAddress DUMMY_SERVER_ADDRESS = new ServerAddress(); private static final BsonDocument DUMMY_ERROR_DETAILS = new BsonDocument(); - private static final BulkWriteResult BULK_WRITE_RESULT = BulkWriteResult.acknowledged( - 1, 0, 2, 3, emptyList(), List.of(new BulkWriteInsert(0, new BsonObjectId(new ObjectId(1, 2))))); + private static final BulkWriteResult BULK_WRITE_RESULT = + BulkWriteResult.acknowledged(0, 0, 0, 0, emptyList(), emptyList()); private static final MongoBulkWriteException MONGO_BULK_WRITE_EXCEPTION_WITH_WRITE_ERRORS = new MongoBulkWriteException( BULK_WRITE_RESULT, @@ -278,20 +629,15 @@ private static Stream genericMongoExceptions() { new MongoException(5000, DUMMY_EXCEPTION_MESSAGE)); } - @ParameterizedTest(name = "test executeBatch MongoException. Parameters: Parameters: mongoException: {0}") - @MethodSource("genericMongoExceptions") + @ParameterizedTest(name = "test executeBatch MongoException {0}") + @MethodSource({"genericMongoExceptions", "timeoutExceptions"}) void testExecuteBatchMongoException(MongoException mongoException) throws SQLException { doThrow(mongoException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); - - assertExecuteBatchThrowsSqlException(sqlException -> { - assertThatObject(sqlException) - .returns(mongoException.getCode(), SQLException::getErrorCode) - .returns(null, SQLException::getSQLState) - .returns(mongoException, SQLException::getCause); - }); + assertExecuteBatchThrowsSqlException( + sqlException -> assertGenericMongoException(sqlException, mongoException)); } - @ParameterizedTest(name = "test executeUpdate MongoException. Parameters: Parameters: mongoException: {0}") + @ParameterizedTest(name = "test executeUpdate MongoException {0}") @MethodSource({"genericMongoExceptions", "timeoutExceptions"}) void testExecuteUpdateMongoException(MongoException mongoException) throws SQLException { doThrow(mongoException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); @@ -299,7 +645,7 @@ void testExecuteUpdateMongoException(MongoException mongoException) throws SQLEx sqlException -> assertGenericMongoException(sqlException, mongoException)); } - @ParameterizedTest(name = "test executeUQuery MongoException. Parameters: Parameters: mongoException: {0}") + @ParameterizedTest(name = "test executeUQuery MongoException {0}") @MethodSource({"genericMongoExceptions", "timeoutExceptions"}) void testExecuteQueryMongoException(MongoException mongoException) throws SQLException { doThrow(mongoException).when(mongoCollection).aggregate(eq(clientSession), anyList()); @@ -307,30 +653,17 @@ void testExecuteQueryMongoException(MongoException mongoException) throws SQLExc sqlException -> assertGenericMongoException(sqlException, mongoException)); } - @ParameterizedTest(name = "test executeBatch timeout exception. Parameters: mongoTimeoutException: {0}") - @MethodSource("timeoutExceptions") - void testExecuteBatchTimeoutException(MongoException mongoTimeoutException) throws SQLException { - doThrow(mongoTimeoutException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); - assertExecuteBatchThrowsSqlException(batchUpdateException -> { - assertGenericMongoException(batchUpdateException, mongoTimeoutException); - }); - } - @Test void testExecuteBatchRuntimeExceptionCause() throws SQLException { - RuntimeException runtimeException = new RuntimeException(); + var runtimeException = new RuntimeException(); doThrow(runtimeException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); - assertExecuteBatchThrowsSqlException(sqlException -> { - assertThatObject(sqlException) - .returns(0, SQLException::getErrorCode) - .returns(null, SQLException::getSQLState) - .returns(runtimeException, SQLException::getCause); - }); + assertExecuteBatchThrowsSqlException( + sqlException -> assertGenericException(sqlException, runtimeException)); } @Test void testExecuteUpdateRuntimeExceptionCause() throws SQLException { - RuntimeException runtimeException = new RuntimeException(); + var runtimeException = new RuntimeException(); doThrow(runtimeException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); assertExecuteUpdateThrowsSqlException( sqlException -> assertGenericException(sqlException, runtimeException)); @@ -338,13 +671,13 @@ void testExecuteUpdateRuntimeExceptionCause() throws SQLException { @Test void testExecuteQueryRuntimeExceptionCause() throws SQLException { - RuntimeException runtimeException = new RuntimeException(); + var runtimeException = new RuntimeException(); doThrow(runtimeException).when(mongoCollection).aggregate(eq(clientSession), anyList()); assertExecuteQueryThrowsSqlException( sqlException -> assertGenericException(sqlException, runtimeException)); } - private static Stream bulkWriteExceptionsForExecuteUpdate() { + private static Stream testExecuteUpdateMongoBulkWriteException() { return mqlCommands() .flatMap(mqlCommand -> Stream.of( Arguments.of(mqlCommand, MONGO_BULK_WRITE_EXCEPTION_WITH_WRITE_CONCERN_EXCEPTION), @@ -353,18 +686,19 @@ private static Stream bulkWriteExceptionsForExecuteUpdate() { @ParameterizedTest( name = "test executeUpdate MongoBulkWriteException. Parameters: commandName={0}, exception={1}") - @MethodSource("bulkWriteExceptionsForExecuteUpdate") + @MethodSource void testExecuteUpdateMongoBulkWriteException(String mql, MongoBulkWriteException mongoBulkWriteException) throws SQLException { doThrow(mongoBulkWriteException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); - Integer vendorCodeError = getVendorCodeError(mongoBulkWriteException); + var vendorErrorCode = getVendorErrorCode(mongoBulkWriteException); - try (MongoPreparedStatement mongoPreparedStatement = createMongoPreparedStatement(mql)) { + try (var mongoPreparedStatement = createMongoPreparedStatement(mql)) { assertThatExceptionOfType(SQLException.class) .isThrownBy(mongoPreparedStatement::executeUpdate) - .withCause(mongoBulkWriteException) - .returns(vendorCodeError, SQLException::getErrorCode) - .returns(null, SQLException::getSQLState); + .returns(vendorErrorCode, SQLException::getErrorCode) + .returns(null, SQLException::getSQLState) + .havingCause() + .isSameAs(mongoBulkWriteException); } } @@ -374,8 +708,9 @@ private static Stream testExecuteBatchMongoBulkWriteException() { // Error in command 1 Arguments.of( mqlCommand, // MQL command to execute - createMongoBulkWriteException(1), // failed model index + createMongoBulkWriteException(0), // failed model index 0), // expected update count length + Arguments.of(mqlCommand, createMongoBulkWriteException(1), 0), Arguments.of(mqlCommand, createMongoBulkWriteException(2), 0), Arguments.of(mqlCommand, createMongoBulkWriteException(3), 0), @@ -396,21 +731,21 @@ private static Stream testExecuteBatchMongoBulkWriteException() { @ParameterizedTest( name = "test executeBatch MongoBulkWriteException. Parameters: commandName={0}, exception={1}, expectedUpdateCountLength={2}") - @MethodSource("testExecuteBatchMongoBulkWriteException") + @MethodSource void testExecuteBatchMongoBulkWriteException( String mql, MongoBulkWriteException mongoBulkWriteException, int expectedUpdateCountLength) throws SQLException { doThrow(mongoBulkWriteException).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); - Integer vendorCodeError = getVendorCodeError(mongoBulkWriteException); + var vendorErrorCode = getVendorErrorCode(mongoBulkWriteException); - try (MongoPreparedStatement mongoPreparedStatement = createMongoPreparedStatement(mql)) { + try (var mongoPreparedStatement = createMongoPreparedStatement(mql)) { mongoPreparedStatement.addBatch(); mongoPreparedStatement.addBatch(); mongoPreparedStatement.addBatch(); assertThatExceptionOfType(BatchUpdateException.class) .isThrownBy(mongoPreparedStatement::executeBatch) - .returns(vendorCodeError, BatchUpdateException::getErrorCode) + .returns(vendorErrorCode, BatchUpdateException::getErrorCode) .returns(null, BatchUpdateException::getSQLState) .satisfies(ex -> { assertUpdateCounts(ex.getUpdateCounts(), expectedUpdateCountLength); @@ -423,7 +758,7 @@ void testExecuteBatchMongoBulkWriteException( private static void assertGenericException(SQLException sqlException, RuntimeException cause) { assertThatObject(sqlException) .isExactlyInstanceOf(SQLException.class) - .returns(0, SQLException::getErrorCode) + .returns(NO_ERROR_CODE, SQLException::getErrorCode) .returns(null, SQLException::getSQLState) .returns(cause, SQLException::getCause); } @@ -437,7 +772,7 @@ private static void assertGenericMongoException(SQLException sqlException, Mongo } private void assertExecuteBatchThrowsSqlException(ThrowingConsumer asserter) throws SQLException { - try (MongoPreparedStatement mongoPreparedStatement = createMongoPreparedStatement(MQL_ITEMS_INSERT)) { + try (var mongoPreparedStatement = createMongoPreparedStatement(MQL_ITEMS_INSERT)) { mongoPreparedStatement.addBatch(); assertThatExceptionOfType(SQLException.class) .isThrownBy(mongoPreparedStatement::executeBatch) @@ -448,7 +783,7 @@ private void assertExecuteBatchThrowsSqlException(ThrowingConsumer private void assertExecuteUpdateThrowsSqlException(ThrowingConsumer asserter) throws SQLException { - try (MongoPreparedStatement mongoPreparedStatement = createMongoPreparedStatement(MQL_ITEMS_INSERT)) { + try (var mongoPreparedStatement = createMongoPreparedStatement(MQL_ITEMS_INSERT)) { assertThatExceptionOfType(SQLException.class) .isThrownBy(mongoPreparedStatement::executeUpdate) .satisfies(asserter); @@ -456,7 +791,7 @@ private void assertExecuteUpdateThrowsSqlException(ThrowingConsumer asserter) throws SQLException { - try (MongoPreparedStatement mongoPreparedStatement = createMongoPreparedStatement(MQL_ITEMS_AGGREGATE)) { + try (var mongoPreparedStatement = createMongoPreparedStatement(MQL_ITEMS_AGGREGATE)) { assertThatExceptionOfType(SQLException.class) .isThrownBy(mongoPreparedStatement::executeQuery) .satisfies(asserter); @@ -465,36 +800,25 @@ private void assertExecuteQueryThrowsSqlException(ThrowingConsumer private static void assertUpdateCounts(int[] actualUpdateCounts, int expectedUpdateCountsLength) { assertEquals(expectedUpdateCountsLength, actualUpdateCounts.length); - for (int count : actualUpdateCounts) { + for (var count : actualUpdateCounts) { assertEquals(SUCCESS_NO_INFO, count); } } - private static MongoBulkWriteException createMongoBulkWriteException(int errorCode, int failedModelIndex) { - return new MongoBulkWriteException( - BULK_WRITE_RESULT, - List.of(new BulkWriteError( - errorCode, DUMMY_EXCEPTION_MESSAGE, DUMMY_ERROR_DETAILS, failedModelIndex)), - null, - DUMMY_SERVER_ADDRESS, - emptySet()); - } - private static MongoBulkWriteException createMongoBulkWriteException(int failedModelIndex) { return new MongoBulkWriteException( BULK_WRITE_RESULT, - List.of(new BulkWriteError( - failedModelIndex, DUMMY_EXCEPTION_MESSAGE, DUMMY_ERROR_DETAILS, failedModelIndex)), + List.of(new BulkWriteError(1, DUMMY_EXCEPTION_MESSAGE, DUMMY_ERROR_DETAILS, failedModelIndex)), null, DUMMY_SERVER_ADDRESS, emptySet()); } - private static Integer getVendorCodeError(MongoBulkWriteException mongoBulkWriteException) { + private static int getVendorErrorCode(MongoBulkWriteException mongoBulkWriteException) { return mongoBulkWriteException.getWriteErrors().stream() .map(BulkWriteError::getCode) .findFirst() - .orElse(0); + .orElse(NO_ERROR_CODE); } } @@ -511,10 +835,7 @@ void testParameterIndexOverflow() throws SQLSyntaxErrorException { } @Nested - class ExecuteMethodClosesLastOpenResultSetTests { - - @Mock - MongoCollection mongoCollection; + class ExecuteClosesLastOpenResultSetTests { @Mock AggregateIterable aggregateIterable; diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java index 4b57d7b2..a59927c3 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; -import com.mongodb.bulk.BulkWriteInsert; import com.mongodb.bulk.BulkWriteResult; import com.mongodb.client.AggregateIterable; import com.mongodb.client.ClientSession; @@ -45,8 +44,6 @@ import java.util.List; import java.util.function.BiConsumer; import org.bson.BsonDocument; -import org.bson.BsonObjectId; -import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -88,9 +85,7 @@ void testNoopWhenCloseStatementClosed() throws SQLException { @Test void testResultSetClosedWhenStatementClosed( - @Mock MongoCollection mongoCollection, - @Mock AggregateIterable aggregateIterable, - @Mock MongoCursor mongoCursor) + @Mock AggregateIterable aggregateIterable, @Mock MongoCursor mongoCursor) throws SQLException { doReturn(mongoCollection).when(mongoDatabase).getCollection(anyString(), eq(BsonDocument.class)); @@ -116,7 +111,7 @@ void testResultSetClosedWhenStatementClosed( @Nested class ExecuteMethodClosesLastOpenResultSetTests { - private final String exampleQueryMql = + private static final String EXAMPLE_QUERY_MQL = """ { aggregate: "books", @@ -125,7 +120,7 @@ class ExecuteMethodClosesLastOpenResultSetTests { { $project: { _id: 0, title: 1, publishYear: 1 } } ] }"""; - private final String exampleUpdateMql = + private static final String EXAMPLE_UPDATE_MQL = """ { update: "members", @@ -144,9 +139,6 @@ class ExecuteMethodClosesLastOpenResultSetTests { @Mock MongoCursor mongoCursor; - private static final BulkWriteResult BULK_WRITE_RESULT = BulkWriteResult.acknowledged( - 1, 0, 2, 3, emptyList(), List.of(new BulkWriteInsert(0, new BsonObjectId(new ObjectId(1, 1))))); - private ResultSet lastOpenResultSet; @BeforeEach @@ -155,28 +147,29 @@ void beforeEach() throws SQLException { doReturn(aggregateIterable).when(mongoCollection).aggregate(same(clientSession), anyList()); doReturn(mongoCursor).when(aggregateIterable).cursor(); - lastOpenResultSet = mongoStatement.executeQuery(exampleQueryMql); + lastOpenResultSet = mongoStatement.executeQuery(EXAMPLE_QUERY_MQL); assertFalse(lastOpenResultSet.isClosed()); } @Test void testExecuteQuery() throws SQLException { - mongoStatement.executeQuery(exampleQueryMql); + mongoStatement.executeQuery(EXAMPLE_QUERY_MQL); assertTrue(lastOpenResultSet.isClosed()); } @Test void testExecuteUpdate() throws SQLException { - doReturn(mongoCollection).when(mongoDatabase).getCollection(anyString(), eq(BsonDocument.class)); - doReturn(BULK_WRITE_RESULT).when(mongoCollection).bulkWrite(eq(clientSession), anyList()); + doReturn(BulkWriteResult.acknowledged(0, 0, 0, 0, emptyList(), emptyList())) + .when(mongoCollection) + .bulkWrite(eq(clientSession), anyList()); - mongoStatement.executeUpdate(exampleUpdateMql); + mongoStatement.executeUpdate(EXAMPLE_UPDATE_MQL); assertTrue(lastOpenResultSet.isClosed()); } @Test void testExecute() throws SQLException { - assertThrows(SQLFeatureNotSupportedException.class, () -> mongoStatement.execute(exampleUpdateMql)); + assertThrows(SQLFeatureNotSupportedException.class, () -> mongoStatement.execute(EXAMPLE_UPDATE_MQL)); assertTrue(lastOpenResultSet.isClosed()); } }