configValues) {
var clientSettings = clientSettingsBuilder.build();
- assertNotNull(BuildConfig.NAME);
- assertNotNull(BuildConfig.VERSION);
var driverInfo = MongoDriverInformation.builder()
- .driverName(BuildConfig.NAME)
- .driverVersion(BuildConfig.VERSION)
+ .driverName(assertNotNull(BuildConfig.NAME))
+ .driverVersion(assertNotNull(BuildConfig.VERSION))
.build();
mongoClient = MongoClients.create(clientSettings, driverInfo);
diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoDatabaseMetaData.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoDatabaseMetaData.java
new file mode 100644
index 00000000..c503e665
--- /dev/null
+++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoDatabaseMetaData.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2024-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.jdbc;
+
+import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue;
+import static com.mongodb.hibernate.internal.MongoAssertions.fail;
+import static com.mongodb.hibernate.internal.VisibleForTesting.AccessModifier.PRIVATE;
+
+import com.mongodb.hibernate.internal.VisibleForTesting;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+
+/**
+ * MongoDB Dialect's JDBC {@link java.sql.DatabaseMetaData} implementation class.
+ *
+ * It only focuses on API methods MongoDB Dialect will support. All the other methods are implemented by throwing
+ * exceptions in its parent {@link DatabaseMetaDataAdapter adapter interface}.
+ */
+final class MongoDatabaseMetaData implements DatabaseMetaDataAdapter {
+
+ public static final String MONGO_DATABASE_PRODUCT_NAME = "MongoDB";
+ public static final String MONGO_JDBC_DRIVER_NAME = "MongoDB Java Driver JDBC Adapter";
+
+ @VisibleForTesting(otherwise = PRIVATE)
+ record VersionNumPair(int majorVersion, int minVersion) {}
+
+ private final Connection connection;
+
+ private final Version databaseVersion;
+ private final Version driverVersion;
+
+ MongoDatabaseMetaData(
+ Connection connection,
+ String databaseVersionText,
+ int databaseMajorVersion,
+ int databaseMinorVersion,
+ String driverVersionText) {
+ this.connection = connection;
+ databaseVersion = new Version(databaseVersionText, databaseMajorVersion, databaseMinorVersion);
+ driverVersion = Version.parse(driverVersionText);
+ }
+
+ @Override
+ public String getDatabaseProductName() {
+ return MONGO_DATABASE_PRODUCT_NAME;
+ }
+
+ @Override
+ public String getDatabaseProductVersion() {
+ return databaseVersion.versionText;
+ }
+
+ @Override
+ public String getDriverName() {
+ return MONGO_JDBC_DRIVER_NAME;
+ }
+
+ @Override
+ public String getDriverVersion() {
+ return driverVersion.versionText;
+ }
+
+ @Override
+ public int getDriverMajorVersion() {
+ return driverVersion.major;
+ }
+
+ @Override
+ public int getDriverMinorVersion() {
+ return driverVersion.minor;
+ }
+
+ @Override
+ public String getSQLKeywords() {
+ return "";
+ }
+
+ @Override
+ public boolean isCatalogAtStart() {
+ return true;
+ }
+
+ @Override
+ public String getCatalogSeparator() {
+ return ".";
+ }
+
+ @Override
+ public boolean supportsSchemasInTableDefinitions() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsCatalogsInTableDefinitions() {
+ return false;
+ }
+
+ @Override
+ public boolean dataDefinitionCausesTransactionCommit() {
+ return false;
+ }
+
+ @Override
+ public boolean dataDefinitionIgnoredInTransactions() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsResultSetType(int type) {
+ return type == ResultSet.TYPE_FORWARD_ONLY;
+ }
+
+ @Override
+ public boolean supportsBatchUpdates() {
+ return true;
+ }
+
+ @Override
+ public Connection getConnection() {
+ return connection;
+ }
+
+ @Override
+ public boolean supportsNamedParameters() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsGetGeneratedKeys() {
+ return false;
+ }
+
+ @Override
+ public int getDatabaseMajorVersion() {
+ return databaseVersion.major;
+ }
+
+ @Override
+ public int getDatabaseMinorVersion() {
+ return databaseVersion.minor;
+ }
+
+ @Override
+ public int getJDBCMajorVersion() {
+ return 4;
+ }
+
+ @Override
+ public int getJDBCMinorVersion() {
+ return 3;
+ }
+
+ @Override
+ public int getSQLStateType() {
+ return DatabaseMetaData.sqlStateSQL;
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) {
+ return false;
+ }
+
+ private record Version(String versionText, int major, int minor) {
+ static Version parse(String versionText) {
+ String[] parts = versionText.split("[-.]", 3);
+ assertTrue(parts.length >= 2);
+ try {
+ return new Version(versionText, Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
+ } catch (NumberFormatException e) {
+ throw fail(e.toString());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java
index f9be5d43..16c86f37 100644
--- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java
+++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java
@@ -205,7 +205,8 @@ public boolean isClosed() {
}
@Override
- public boolean isWrapperFor(Class> iface) {
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ checkClosed();
return false;
}
diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionProviderTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionProviderTests.java
index 77ca53fc..1138a3ad 100644
--- a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionProviderTests.java
+++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionProviderTests.java
@@ -94,7 +94,7 @@ void testMongoClientCustomizerTakeEffect(boolean customizerAppliesConnectionStri
@Test
void testMongoClientCustomizerThrowException() {
- assertThrows(ServiceException.class, () -> {
+ assertThrows(NullPointerException.class, () -> {
try (var ignored = buildSessionFactory(
(builder, cs) -> {
throw new NullPointerException();
diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java
index 2c5dda28..42add1ac 100644
--- a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java
+++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java
@@ -16,26 +16,39 @@
package com.mongodb.hibernate.jdbc;
+import static com.mongodb.hibernate.jdbc.MongoDatabaseMetaData.MONGO_DATABASE_PRODUCT_NAME;
+import static com.mongodb.hibernate.jdbc.MongoDatabaseMetaData.MONGO_JDBC_DRIVER_NAME;
+import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.mongodb.client.ClientSession;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoDatabase;
import java.sql.Connection;
+import java.sql.ResultSet;
import java.sql.SQLException;
-import java.sql.SQLFeatureNotSupportedException;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.bson.Document;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.InjectMocks;
import org.mockito.Mock;
@@ -47,6 +60,9 @@ class MongoConnectionTests {
@Mock
private ClientSession clientSession;
+ @Mock
+ private MongoClient mongoClient;
+
@InjectMocks
private MongoConnection mongoConnection;
@@ -80,6 +96,76 @@ void testClosedWhenSessionClosingThrowsException() {
}
}
+ @Nested
+ class ClosedTests {
+
+ @FunctionalInterface
+ interface ConnectionMethodInvocation {
+ void runOn(MongoConnection conn) throws SQLException;
+ }
+
+ @ParameterizedTest(name = "SQLException is thrown when \"{0}\" is called on a closed MongoConnection")
+ @MethodSource("getMongoConnectionMethodInvocationsImpactedByClosing")
+ void testCheckClosed(String label, ConnectionMethodInvocation methodInvocation) throws SQLException {
+ // given
+ mongoConnection.close();
+
+ // when && then
+ var exception = assertThrows(SQLException.class, () -> methodInvocation.runOn(mongoConnection));
+ assertEquals("Connection has been closed", exception.getMessage());
+ }
+
+ private static Stream getMongoConnectionMethodInvocationsImpactedByClosing() {
+ var exampleQueryMql =
+ """
+ {
+ find: "restaurants",
+ filter: { rating: { $gte: 9 }, cuisine: "italian" },
+ projection: { name: 1, rating: 1, address: 1 },
+ sort: { name: 1 },
+ limit: 5
+ }""";
+ var exampleUpdateMql =
+ """
+ {
+ update: "members",
+ updates: [
+ {
+ q: {},
+ u: { $inc: { points: 1 } },
+ multi: true
+ }
+ ]
+ }""";
+ return Map.ofEntries(
+ Map.entry("setAutoCommit(boolean)", conn -> conn.setAutoCommit(false)),
+ Map.entry("getAutoCommit()", MongoConnection::getAutoCommit),
+ Map.entry("commit()", MongoConnection::commit),
+ Map.entry("rollback()", MongoConnection::rollback),
+ Map.entry("createStatement()", MongoConnection::createStatement),
+ Map.entry("prepareStatement(String)", conn -> conn.prepareStatement(exampleUpdateMql)),
+ Map.entry(
+ "prepareStatement(String,int,int)",
+ conn -> conn.prepareStatement(
+ exampleQueryMql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)),
+ Map.entry(
+ "createArrayOf(String,Object[])",
+ conn -> conn.createArrayOf("myArrayType", new String[] {"value1", "value2"})),
+ Map.entry(
+ "createStruct(String,Object[])",
+ conn -> conn.createStruct("myStructType", new Object[] {1, "Toronto"})),
+ Map.entry("getMetaData()", MongoConnection::getMetaData),
+ Map.entry("getCatalog()", MongoConnection::getCatalog),
+ Map.entry("getSchema()", MongoConnection::getSchema),
+ Map.entry("getWarnings()", MongoConnection::getWarnings),
+ Map.entry("clearWarnings()", MongoConnection::clearWarnings),
+ Map.entry("isWrapperFor()", conn -> conn.isWrapperFor(Connection.class)))
+ .entrySet()
+ .stream()
+ .map(entry -> Arguments.of(entry.getKey(), entry.getValue()));
+ }
+ }
+
@Nested
class TransactionTests {
@@ -156,19 +242,6 @@ void testNoTransactionStartedWhenAutoCommitChangedToFalse() throws SQLException
verify(clientSession, never()).startTransaction();
}
}
-
- @ParameterizedTest(
- name = "SQLException is thrown when 'setAutoCommit({0})' is called on a closed MongoConnection")
- @ValueSource(booleans = {true, false})
- void testSQLExceptionThrowWhenCalledOnClosedConnection(boolean autoCommit) throws SQLException {
- // given
- mongoConnection.close();
- verify(clientSession).close();
-
- // when && then
- assertThrows(SQLException.class, () -> mongoConnection.setAutoCommit(autoCommit));
- verifyNoMoreInteractions(clientSession);
- }
}
@Nested
@@ -207,18 +280,6 @@ void testSQLExceptionThrownWhenTransactionCommitFailed() throws SQLException {
// when && then
assertThrows(SQLException.class, () -> mongoConnection.commit());
}
-
- @Test
- @DisplayName("SQLException is thrown when 'commit()' is called on a closed MongoConnection")
- void testSQLExceptionThrowWhenCalledOnClosedConnection() throws SQLException {
- // given
- mongoConnection.close();
- verify(clientSession).close();
-
- // when && then
- assertThrows(SQLException.class, () -> mongoConnection.commit());
- verifyNoMoreInteractions(clientSession);
- }
}
@Nested
@@ -257,81 +318,56 @@ void testSQLExceptionThrownWhenTransactionRollbackFailed() throws SQLException {
// when && then
assertThrows(SQLException.class, () -> mongoConnection.rollback());
}
-
- @Test
- @DisplayName("SQLException is thrown when 'rollback()' is called on a closed MongoConnection")
- void testSQLExceptionThrowWhenCalledOnClosedConnection() throws SQLException {
- // given
- mongoConnection.close();
- verify(clientSession).close();
-
- // when && then
- assertThrows(SQLException.class, () -> mongoConnection.rollback());
- verifyNoMoreInteractions(clientSession);
- }
}
+ }
- @Nested
- class TransactionIsolationLevelTests {
-
- @ParameterizedTest
- @ValueSource(
- ints = {
- Connection.TRANSACTION_NONE,
- Connection.TRANSACTION_READ_UNCOMMITTED,
- Connection.TRANSACTION_READ_COMMITTED,
- Connection.TRANSACTION_REPEATABLE_READ,
- Connection.TRANSACTION_SERIALIZABLE
- })
- @DisplayName("MongoDB Dialect doesn't support JDBC transaction isolation level setting")
- void testSetUnsupported(int level) {
- // when && then
- assertThrows(
- SQLFeatureNotSupportedException.class, () -> mongoConnection.setTransactionIsolation(level));
- verifyNoInteractions(clientSession);
- }
+ @Nested
+ class GetMetaDataTests {
- @ParameterizedTest
- @ValueSource(
- ints = {
- Connection.TRANSACTION_NONE,
- Connection.TRANSACTION_READ_UNCOMMITTED,
- Connection.TRANSACTION_READ_COMMITTED,
- Connection.TRANSACTION_REPEATABLE_READ,
- Connection.TRANSACTION_SERIALIZABLE
- })
- @DisplayName(
- "SQLException is thrown when 'setTransactionIsolation({0})' is called on a closed MongoConnection")
- void testSQLExceptionThrowWhenCalledOnClosedConnection(int level) throws SQLException {
- // given
- mongoConnection.close();
- verify(clientSession).close();
+ @Mock
+ private MongoDatabase mongoDatabase;
- // when && then
- assertThrows(SQLException.class, () -> mongoConnection.setTransactionIsolation(level));
- verifyNoMoreInteractions(clientSession);
- }
+ @Test
+ @DisplayName("Happy path for MongoDatabaseMetaData fetching")
+ void testSuccess() {
+ // given
+ doReturn(mongoDatabase).when(mongoClient).getDatabase(eq("admin"));
+ var commandResultJson =
+ """
+ {
+ "ok": 1.0,
+ "version": "8.0.1",
+ "versionArray": [8, 0, 1, 0]
+ }""";
+ var commandResultDoc = Document.parse(commandResultJson);
+ doReturn(commandResultDoc)
+ .when(mongoDatabase)
+ .runCommand(any(ClientSession.class), argThat(arg -> "buildinfo"
+ .equals(arg.toBsonDocument().getFirstKey())));
- @Test
- @DisplayName("MongoDB Dialect doesn't support JDBC transaction isolation level fetching")
- void testGetUnsupported() {
- // when && then
- assertThrows(SQLFeatureNotSupportedException.class, () -> mongoConnection.getTransactionIsolation());
- verifyNoInteractions(clientSession);
- }
+ // when
+ var metaData = assertDoesNotThrow(() -> mongoConnection.getMetaData());
- @Test
- @DisplayName(
- "SQLException is thrown when 'getTransactionIsolation()' is called on a closed MongoConnection")
- void testSQLExceptionThrowWhenCalledOnClosedConnection() throws SQLException {
- // given
- mongoConnection.close();
- verify(clientSession).close();
+ // then
+ assertAll(
+ () -> assertEquals(MONGO_DATABASE_PRODUCT_NAME, metaData.getDatabaseProductName()),
+ () -> assertEquals(MONGO_JDBC_DRIVER_NAME, metaData.getDriverName()),
+ () -> assertEquals("8.0.1", metaData.getDatabaseProductVersion()),
+ () -> assertEquals(8, metaData.getDatabaseMajorVersion()),
+ () -> assertEquals(0, metaData.getDatabaseMinorVersion()));
+ }
- // when && then
- assertThrows(SQLException.class, () -> mongoConnection.getTransactionIsolation());
- verifyNoMoreInteractions(clientSession);
- }
+ @Test
+ @DisplayName("SQLException is thrown when MongoConnection#getMetaData() failed while interacting with db")
+ void testSQLExceptionThrownWhenMetaDataFetchingFailed() {
+ // given
+ doReturn(mongoDatabase).when(mongoClient).getDatabase(eq("admin"));
+ doThrow(new RuntimeException())
+ .when(mongoDatabase)
+ .runCommand(any(ClientSession.class), argThat(arg -> "buildinfo"
+ .equals(arg.toBsonDocument().getFirstKey())));
+ // when && then
+ assertThrows(SQLException.class, () -> mongoConnection.getMetaData());
}
}
}
diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoDatabaseMetaDataTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoDatabaseMetaDataTests.java
new file mode 100644
index 00000000..309df6f9
--- /dev/null
+++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoDatabaseMetaDataTests.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2025-present MongoDB, Inc.
+ *
+ * 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.
+ */
+
+package com.mongodb.hibernate.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.sql.Connection;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.Mock;
+
+class MongoDatabaseMetaDataTests {
+ @Mock
+ private Connection connection;
+
+ @ParameterizedTest
+ @CsvSource({"3.2, 3, 2", "1.0.0-SNAPSHOT, 1, 0", "5.2.0-alpha, 5, 2", "2.1-beta, 2, 1", "1-0, 1, 0"})
+ void constructor(String driverVersionText, int driverMajorVersion, int driverMinorVersion) {
+ MongoDatabaseMetaData metadata = new MongoDatabaseMetaData(connection, "DBMS 1.2", 1, 2, driverVersionText);
+ assertAll(
+ () -> assertSame(connection, metadata.getConnection()),
+ () -> assertEquals("DBMS 1.2", metadata.getDatabaseProductVersion()),
+ () -> assertEquals(1, metadata.getDatabaseMajorVersion()),
+ () -> assertEquals(2, metadata.getDatabaseMinorVersion()),
+ () -> assertEquals(driverVersionText, metadata.getDriverVersion()),
+ () -> assertEquals(driverMajorVersion, metadata.getDriverMajorVersion()),
+ () -> assertEquals(driverMinorVersion, metadata.getDriverMinorVersion()));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"3", "alpha", "."})
+ void constructorFails(String invalidDriverVersionText) {
+ assertThrows(
+ AssertionError.class,
+ () -> new MongoDatabaseMetaData(connection, "DBMS 1.2", 1, 2, invalidDriverVersionText));
+ }
+}
diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java
index 90ba779b..3f4ea3f3 100644
--- a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java
+++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java
@@ -31,6 +31,7 @@
import com.mongodb.client.MongoDatabase;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
+import java.sql.Statement;
import java.util.Map;
import java.util.stream.Stream;
import org.bson.BsonDocument;
@@ -168,7 +169,8 @@ private static Stream getMongoStatementMethodInvocationsImpactedByClo
Map.entry("addBatch(String)", stmt -> stmt.addBatch(exampleUpdateMql)),
Map.entry("clearBatch()", MongoStatement::clearBatch),
Map.entry("executeBatch()", MongoStatement::executeBatch),
- Map.entry("getConnection()", MongoStatement::getConnection))
+ Map.entry("getConnection()", MongoStatement::getConnection),
+ Map.entry("isWrapperFor(Class)", stmt -> stmt.isWrapperFor(Statement.class)))
.entrySet()
.stream()
.map(entry -> Arguments.of(entry.getKey(), entry.getValue()));
diff --git a/src/test/resources/hibernate.properties b/src/test/resources/hibernate.properties
index 162cadb2..98d656f9 100644
--- a/src/test/resources/hibernate.properties
+++ b/src/test/resources/hibernate.properties
@@ -1,3 +1,4 @@
jakarta.persistence.jdbc.url=mongodb://localhost/mongo-hibernate-test?directConnection=false
hibernate.dialect=com.mongodb.hibernate.dialect.MongoDialect
hibernate.connection.provider_class=com.mongodb.hibernate.jdbc.MongoConnectionProvider
+hibernate.boot.allow_jdbc_metadata_access=false