From 5c70ee8acbd7d2b3d8cbbe66dc67ace596fa0d27 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 16 Dec 2024 16:46:06 -0500 Subject: [PATCH 01/38] implement java.sql.PreparedStatement --- gradle/libs.versions.toml | 2 +- ...ongoPreparedStatementIntegrationTests.java | 204 ++++++++++ .../hibernate/jdbc/MongoConnection.java | 5 +- .../jdbc/MongoPreparedStatement.java | 313 +++++++++++++++ .../hibernate/jdbc/MongoStatement.java | 11 +- .../jdbc/PreparedStatementAdapter.java | 333 +++++++++++++++ .../jdbc/MongoPreparedStatementTests.java | 379 ++++++++++++++++++ .../hibernate/jdbc/MongoStatementTests.java | 2 +- 8 files changed, 1241 insertions(+), 8 deletions(-) create mode 100644 src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java create mode 100644 src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java create mode 100644 src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java create mode 100644 src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d81d2fe3..5518251d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ google-errorprone-core = "2.36.0" nullaway = "0.12.2" jspecify = "1.0.0" hibernate-core = "6.6.4.Final" -mongo-java-driver-sync = "5.2.1" +mongo-java-driver-sync = "5.3.0" slf4j-api = "2.0.16" logback-classic = "1.5.15" mockito = "5.14.2" diff --git a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java new file mode 100644 index 00000000..e7bbe144 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java @@ -0,0 +1,204 @@ +/* + * 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import org.bson.BsonDocument; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MongoPreparedStatementIntegrationTests { + + private static @Nullable SessionFactory sessionFactory; + + private @Nullable Session session; + + @BeforeAll + static void beforeAll() { + sessionFactory = new Configuration().buildSessionFactory(); + } + + @AfterAll + static void afterAll() { + if (sessionFactory != null) { + sessionFactory.close(); + } + } + + @BeforeEach + void setUp() { + session = assertNotNull(sessionFactory).openSession(); + } + + @AfterEach + void tearDown() { + if (session != null) { + session.close(); + } + } + + @Nested + class ExecuteUpdateTests { + + @BeforeEach + void setUp() { + assertNotNull(session).doWork(conn -> { + conn.createStatement() + .executeUpdate( + """ + { + delete: "books", + deletes: [ + { q: {}, limit: 0 } + ] + }"""); + }); + } + + private static final String INSERT_MQL = + """ + { + insert: "books", + documents: [ + { + _id: 1, + title: "War and Peace", + author: "Leo Tolstoy", + outOfStock: false + }, + { + _id: 2, + title: "Anna Karenina", + author: "Leo Tolstoy", + outOfStock: false + }, + { + _id: 3, + title: "Crime and Punishment", + author: "Fyodor Dostoevsky", + outOfStock: false + } + ] + }"""; + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testUpdate(boolean autoCommit) { + // given + prepareData(INSERT_MQL); + + // when && then + var expectedDocs = Set.of( + BsonDocument.parse( + """ + { + _id: 1, + title: "War and Peace", + author: "Leo Tolstoy", + outOfStock: true + }"""), + BsonDocument.parse( + """ + { + _id: 2, + title: "Anna Karenina", + author: "Leo Tolstoy", + outOfStock: true + }"""), + BsonDocument.parse( + """ + { + _id: 3, + title: "Crime and Punishment", + author: "Fyodor Dostoevsky", + outOfStock: false + }""")); + Function pstmtProvider = connection -> { + try { + var pstmt = (MongoPreparedStatement) + connection.prepareStatement( + """ + { + update: "books", + updates: [ + { + q: { author: { $undefined: true } }, + u: { + $set: { outOfStock: { $undefined: true } } + }, + multi: true + } + ] + }"""); + pstmt.setString(1, "Leo Tolstoy"); + pstmt.setBoolean(2, true); + return pstmt; + } catch (SQLException e) { + throw new RuntimeException(e); + } + }; + assertExecuteUpdate(pstmtProvider, autoCommit, 2, expectedDocs); + } + + private void prepareData(String mql) { + assertNotNull(session).doWork(connection -> { + connection.setAutoCommit(true); + var statement = connection.createStatement(); + statement.executeUpdate(mql); + }); + } + + private void assertExecuteUpdate( + Function pstmtProvider, + boolean autoCommit, + int expectedRowCount, + Set expectedDocuments) { + assertNotNull(session).doWork(connection -> { + connection.setAutoCommit(autoCommit); + var pstmt = pstmtProvider.apply(connection); + try { + assertEquals(expectedRowCount, pstmt.executeUpdate()); + } finally { + if (!autoCommit) { + connection.commit(); + } + } + var realDocuments = pstmt.getMongoDatabase() + .getCollection("books", BsonDocument.class) + .find() + .into(new HashSet<>()); + assertEquals(expectedDocuments, realDocuments); + }); + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java index 7379e48e..0c869fef 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java @@ -143,8 +143,7 @@ public Statement createStatement() throws SQLException { @Override public PreparedStatement prepareStatement(String mql) throws SQLException { checkClosed(); - throw new NotYetImplementedException( - "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-13"); + return new MongoPreparedStatement(mongoClient, clientSession, this, mql); } @Override @@ -152,7 +151,7 @@ public PreparedStatement prepareStatement(String mql, int resultSetType, int res throws SQLException { checkClosed(); throw new NotYetImplementedException( - "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-13"); + "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-21"); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java new file mode 100644 index 00000000..9878f31f --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -0,0 +1,313 @@ +/* + * 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 com.mongodb.client.ClientSession; +import com.mongodb.client.MongoClient; +import com.mongodb.hibernate.internal.NotYetImplementedException; +import com.mongodb.hibernate.internal.VisibleForTesting; +import java.io.InputStream; +import java.math.BigDecimal; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonBoolean; +import org.bson.BsonDateTime; +import org.bson.BsonDecimal128; +import org.bson.BsonDocument; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonNull; +import org.bson.BsonString; +import org.bson.BsonType; +import org.bson.BsonValue; +import org.bson.types.Decimal128; +import org.jspecify.annotations.Nullable; + +/** + * MongoDB Dialect's JDBC {@link java.sql.PreparedStatement} 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 PreparedStatementAdapter adapter interface}. + */ +final class MongoPreparedStatement extends MongoStatement implements PreparedStatementAdapter { + + private final BsonDocument command; + + // once parameter is provided, the corresponding entry will be set to null + // so duplicated parameter setting could be detected + private final List<@Nullable Consumer> parameters; + + private int unresolvedParameterCount; + + public MongoPreparedStatement( + MongoClient mongoClient, ClientSession clientSession, MongoConnection mongoConnection, String mql) { + super(mongoClient, clientSession, mongoConnection); + this.command = BsonDocument.parse(mql); + this.parameters = new ArrayList<>(); + parseParameters(command, parameters); + unresolvedParameterCount = parameters.size(); + } + + @Override + public ResultSet executeQuery() throws SQLException { + checkClosed(); + ensureAllParametersResolved(); + throw new NotYetImplementedException(); + } + + @Override + public int executeUpdate() throws SQLException { + checkClosed(); + ensureAllParametersResolved(); + return executeUpdate(command); + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + checkClosed(); + setParameter(parameterIndex, BsonNull.VALUE); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + setParameter(parameterIndex, BsonBoolean.valueOf(x)); + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + setInt(parameterIndex, x); + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + setInt(parameterIndex, x); + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + setParameter(parameterIndex, new BsonInt32(x)); + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + setParameter(parameterIndex, new BsonInt64(x)); + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + setDouble(parameterIndex, x); + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + setParameter(parameterIndex, new BsonDouble(x)); + } + + @Override + public void setBigDecimal(int parameterIndex, @Nullable BigDecimal x) throws SQLException { + setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonDecimal128(new Decimal128(x))); + } + + @Override + public void setString(int parameterIndex, @Nullable String x) throws SQLException { + setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonString(x)); + } + + @Override + public void setBytes(int parameterIndex, byte @Nullable [] x) throws SQLException { + setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonBinary(x)); + } + + @Override + public void setDate(int parameterIndex, @Nullable Date x) throws SQLException { + setBsonDateTimeParameter(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, @Nullable Time x) throws SQLException { + setBsonDateTimeParameter(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, @Nullable Timestamp x) throws SQLException { + setBsonDateTimeParameter(parameterIndex, x); + } + + @Override + public void setBinaryStream(int parameterIndex, @Nullable InputStream x, int length) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + // ---------------------------------------------------------------------- + // Advanced features: + + @Override + public void setObject(int parameterIndex, @Nullable Object x, int targetSqlType) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setObject(int parameterIndex, @Nullable Object x) throws SQLException { + setObject(parameterIndex, x, Types.OTHER); + } + + // --------------------------JDBC 2.0----------------------------- + + @Override + public void addBatch() throws SQLException { + checkClosed(); + throw new NotYetImplementedException( + "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-35"); + } + + @Override + public void setBlob(int parameterIndex, @Nullable Blob x) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setClob(int parameterIndex, @Nullable Clob x) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setArray(int parameterIndex, @Nullable Array x) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setDate(int parameterIndex, @Nullable Date x, @Nullable Calendar cal) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setTime(int parameterIndex, @Nullable Time x, @Nullable Calendar cal) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Calendar cal) throws SQLException { + checkClosed(); + throw new NotYetImplementedException(); + } + + @Override + public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) throws SQLException { + checkClosed(); + setParameter(parameterIndex, BsonNull.VALUE); + } + + private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Date date) throws SQLException { + setParameter(parameterIndex, date == null ? BsonNull.VALUE : new BsonDateTime(date.getTime())); + } + + private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { + checkClosed(); + if (parameterIndex <= 0) { + throw new SQLException(ERROR_MSG_PARAMETER_INDEX_UNDERFLOW); + } + if (parameterIndex > parameters.size()) { + throw new SQLException(ERROR_MSG_PARAMETER_INDEX_OVERFLOW); + } + var parameterValueConsumer = parameters.get(parameterIndex - 1); + if (parameterValueConsumer == null) { + throw new SQLException(ERROR_MSG_PARAMETER_VALUE_SET_MORE_THAN_ONCE); + } + parameterValueConsumer.accept(parameterValue); + parameters.set(parameterIndex - 1, null); + unresolvedParameterCount--; + } + + private static void parseParameters(BsonDocument command, List<@Nullable Consumer> parameters) { + for (var entry : command.entrySet()) { + if (isParameterMarker(entry.getValue())) { + parameters.add(entry::setValue); + } else if (entry.getValue().getBsonType().isContainer()) { + parseParameters(entry.getValue(), parameters); + } + } + } + + private static void parseParameters(BsonArray array, List<@Nullable Consumer> parameters) { + IntStream.range(0, array.size()).forEach(i -> { + var value = array.get(i); + if (isParameterMarker(value)) { + parameters.add(v -> array.set(i, v)); + } else if (value.getBsonType().isContainer()) { + parseParameters(value, parameters); + } + }); + } + + private static void parseParameters(BsonValue value, List<@Nullable Consumer> parameters) { + if (value.isDocument()) { + parseParameters(value.asDocument(), parameters); + } else if (value.isArray()) { + parseParameters(value.asArray(), parameters); + } + } + + private static boolean isParameterMarker(BsonValue value) { + return value.getBsonType() == BsonType.UNDEFINED; + } + + private void ensureAllParametersResolved() throws SQLException { + if (unresolvedParameterCount > 0) { + throw new SQLException("Unresolved parameter found"); + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + List<@Nullable Consumer> getParameters() { + return parameters; + } + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + static final String ERROR_MSG_PARAMETER_INDEX_UNDERFLOW = "Parameter index should start from 1"; + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + static final String ERROR_MSG_PARAMETER_INDEX_OVERFLOW = + "Parameter index should not be larger than parameters size"; + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + static final String ERROR_MSG_PARAMETER_VALUE_SET_MORE_THAN_ONCE = "Parameter index has been set previously"; + + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + static final String ERROR_MSG_PARAMETER_UNRESOLVED = "Unresolved parameter found"; +} diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index 2f0f4f1a..306e0410 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -18,6 +18,7 @@ import static com.mongodb.hibernate.internal.VisibleForTesting.AccessModifier.PRIVATE; import static com.mongodb.hibernate.jdbc.MongoConnection.DATABASE; +import static java.lang.String.format; import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; @@ -38,7 +39,7 @@ *

It only focuses on API methods Mongo Dialect will support. All the other methods are implemented by throwing * exceptions in its parent class. */ -final class MongoStatement extends StatementAdapter { +class MongoStatement extends StatementAdapter { private final MongoClient mongoClient; private final MongoConnection mongoConnection; @@ -63,6 +64,10 @@ public ResultSet executeQuery(String mql) throws SQLException { public int executeUpdate(String mql) throws SQLException { checkClosed(); var command = parse(mql); + return executeUpdate(command); + } + + protected int executeUpdate(BsonDocument command) throws SQLException { startTransactionIfNeeded(); try { return mongoClient @@ -199,9 +204,9 @@ public boolean isClosed() { return closed; } - private void checkClosed() throws SQLException { + protected void checkClosed() throws SQLException { if (closed) { - throw new SQLException("Statement has been closed"); + throw new SQLException(format("%s has been closed", getClass().getSimpleName())); } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java new file mode 100644 index 00000000..13767c86 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java @@ -0,0 +1,333 @@ +/* + * 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 java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +/** + * A {@link java.sql.PreparedStatement} adapter interface that throws exceptions for all its API methods by default. + * + * @see MongoPreparedStatement + */ +interface PreparedStatementAdapter extends PreparedStatement { + @Override + default ResultSet executeQuery() throws SQLException { + throw new SQLFeatureNotSupportedException("executeQuery not implemented"); + } + + @Override + default int executeUpdate() throws SQLException { + throw new SQLFeatureNotSupportedException("executeUpdate not implemented"); + } + + @Override + default void setNull(int parameterIndex, int sqlType) throws SQLException { + throw new SQLFeatureNotSupportedException("setNull not implemented"); + } + + @Override + default void setBoolean(int parameterIndex, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException("setBoolean not implemented"); + } + + @Override + default void setByte(int parameterIndex, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException("setByte not implemented"); + } + + @Override + default void setShort(int parameterIndex, short x) throws SQLException { + throw new SQLFeatureNotSupportedException("setShort not implemented"); + } + + @Override + default void setInt(int parameterIndex, int x) throws SQLException { + throw new SQLFeatureNotSupportedException("setInt not implemented"); + } + + @Override + default void setLong(int parameterIndex, long x) throws SQLException { + throw new SQLFeatureNotSupportedException("setLong not implemented"); + } + + @Override + default void setFloat(int parameterIndex, float x) throws SQLException { + throw new SQLFeatureNotSupportedException("setFloat not implemented"); + } + + @Override + default void setDouble(int parameterIndex, double x) throws SQLException { + throw new SQLFeatureNotSupportedException("setDouble not implemented"); + } + + @Override + default void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException("setBigDecimal not implemented"); + } + + @Override + default void setString(int parameterIndex, String x) throws SQLException { + throw new SQLFeatureNotSupportedException("setString not implemented"); + } + + @Override + default void setBytes(int parameterIndex, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException("setBytes not implemented"); + } + + @Override + default void setDate(int parameterIndex, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException("setDate not implemented"); + } + + @Override + default void setTime(int parameterIndex, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException("setTime not implemented"); + } + + @Override + default void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException("setTimestamp not implemented"); + } + + @Override + default void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("setAsciiStream not implemented"); + } + + @Override + default void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("setUnicodeStream not implemented"); + } + + @Override + default void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("setBinaryStream not implemented"); + } + + @Override + default void clearParameters() throws SQLException { + throw new SQLFeatureNotSupportedException("clearParameters not implemented"); + } + + // ---------------------------------------------------------------------- + // Advanced features: + + @Override + default void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + throw new SQLFeatureNotSupportedException("setObject not implemented"); + } + + @Override + default void setObject(int parameterIndex, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException("setObject not implemented"); + } + + @Override + default boolean execute() throws SQLException { + throw new SQLFeatureNotSupportedException("execute not implemented"); + } + + // --------------------------JDBC 2.0----------------------------- + + @Override + default void addBatch() throws SQLException { + throw new SQLFeatureNotSupportedException("addBatch not implemented"); + } + + @Override + default void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("setCharacterStream not implemented"); + } + + @Override + default void setRef(int parameterIndex, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException("setRef not implemented"); + } + + @Override + default void setBlob(int parameterIndex, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException("setBlob not implemented"); + } + + @Override + default void setClob(int parameterIndex, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException("setClob not implemented"); + } + + @Override + default void setArray(int parameterIndex, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException("setArray not implemented"); + } + + @Override + default ResultSetMetaData getMetaData() throws SQLException { + throw new SQLFeatureNotSupportedException("getMetaData not implemented"); + } + + @Override + default void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException("setDate not implemented"); + } + + @Override + default void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException("setTime not implemented"); + } + + @Override + default void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + throw new SQLFeatureNotSupportedException("setTimestamp not implemented"); + } + + @Override + default void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + throw new SQLFeatureNotSupportedException("setNull not implemented"); + } + + // ------------------------- JDBC 3.0 ----------------------------------- + + @Override + default void setURL(int parameterIndex, URL x) throws SQLException { + throw new SQLFeatureNotSupportedException("setURL not implemented"); + } + + @Override + default ParameterMetaData getParameterMetaData() throws SQLException { + throw new SQLFeatureNotSupportedException("getParameterMetaData not implemented"); + } + + // ------------------------- JDBC 4.0 ----------------------------------- + + @Override + default void setRowId(int parameterIndex, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException("setRowId not implemented"); + } + + @Override + default void setNString(int parameterIndex, String value) throws SQLException { + throw new SQLFeatureNotSupportedException("setNString not implemented"); + } + + @Override + default void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setNCharacterStream not implemented"); + } + + @Override + default void setNClob(int parameterIndex, NClob value) throws SQLException { + throw new SQLFeatureNotSupportedException("setNClob not implemented"); + } + + @Override + default void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setClob not implemented"); + } + + @Override + default void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setBlob not implemented"); + } + + @Override + default void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setNClob not implemented"); + } + + @Override + default void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException("setSQLXML not implemented"); + } + + @Override + default void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException("setObject not implemented"); + } + + @Override + default void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setAsciiStream not implemented"); + } + + @Override + default void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setBinaryStream not implemented"); + } + + @Override + default void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("setCharacterStream not implemented"); + } + + @Override + default void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException("setAsciiStream not implemented"); + } + + @Override + default void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException("setBinaryStream not implemented"); + } + + @Override + default void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("setCharacterStream not implemented"); + } + + @Override + default void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + throw new SQLFeatureNotSupportedException("setNCharacterStream not implemented"); + } + + @Override + default void setClob(int parameterIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("setClob not implemented"); + } + + @Override + default void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException("setBlob not implemented"); + } + + @Override + default void setNClob(int parameterIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("setNClob not implemented"); + } + + // ------------------------- JDBC 4.2 ----------------------------------- + +} diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java new file mode 100644 index 00000000..7febbde5 --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -0,0 +1,379 @@ +/* + * 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.jdbc.MongoPreparedStatement.ERROR_MSG_PARAMETER_INDEX_OVERFLOW; +import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PARAMETER_INDEX_UNDERFLOW; +import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PARAMETER_UNRESOLVED; +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.Answers.RETURNS_SMART_NULLS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoDatabase; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Map; +import java.util.stream.Stream; +import org.bson.BsonDocument; +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.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +class MongoPreparedStatementTests { + + @Mock(answer = RETURNS_SMART_NULLS) + private MongoClient mongoClient; + + @Mock(answer = RETURNS_SMART_NULLS) + private ClientSession clientSession; + + @Mock(answer = RETURNS_SMART_NULLS) + private MongoConnection mongoConnection; + + private MongoPreparedStatement createMongoPreparedStatement(String mql) { + return new MongoPreparedStatement(mongoClient, clientSession, mongoConnection, mql); + } + + @Nested + class ParameterParsingTests { + + @Test + @DisplayName("Empty parameters should be created if no parameter marker found") + void testZeroParameters() { + // given + var mql = + """ + { + insert: "books", + documents: [ + { + title: "War and Peace", + author: "Leo Tolstoy", + outOfStock: false + } + ] + } + """; + + // when && then + try (var preparedStatement = createMongoPreparedStatement(mql)) { + assertTrue(preparedStatement.getParameters().isEmpty()); + } + } + + @Test + @DisplayName("Parameter marker could be found in embedded document field") + void testParameterMarkerInDocumentContainer() { + // given + var mql = + """ + { + insert: "books", + documents: [ + { + title: { $undefined: true }, + author: "Leo Tolstoy", + outOfStock: false + } + ] + } + """; + + // when && then + try (var preparedStatement = createMongoPreparedStatement(mql)) { + assertEquals(1, preparedStatement.getParameters().size()); + } + } + + @Test + @DisplayName("Parameter marker could be found in embedded array field") + void testParameterMarkerInArrayContainer() { + // given + var mql = + """ + { + insert: "books", + documents: [ + { + title: "War and Peace", + author: "Leo Tolstoy", + outOfStock: false, + tags: [ + { $undefined: true }, + { $undefined: true }, + ] + } + ] + } + """; + + // when && then + try (var preparedStatement = createMongoPreparedStatement(mql)) { + assertEquals(2, preparedStatement.getParameters().size()); + } + } + + @Test + @DisplayName("Parameter marker could be found in both document and array embedded fields") + void testParameterMarkerInBothDocumentAndArrayContainers() { + // given + var mql = + """ + { + insert: "books", + documents: [ + { + title: { $undefined: true }, + author: { $undefined: true }, + outOfStock: false, + tags: [ + { $undefined: true } + ] + } + ] + } + """; + + // when && then + try (var preparedStatement = createMongoPreparedStatement(mql)) { + assertEquals(3, preparedStatement.getParameters().size()); + } + } + } + + @Nested + class ParameterValueSettingTests { + + private static final String EXAMPLE_MQL = + """ + { + insert: "books", + documents: [ + { + title: { $undefined: true }, + author: { $undefined: true }, + publishYear: { $undefined: true }, + outOfStock: { $undefined: true }, + tags: [ + { $undefined: true } + ] + } + ] + } + """; + + @Mock(answer = RETURNS_SMART_NULLS) + private MongoDatabase mongoDatabase; + + @Captor + private ArgumentCaptor commandCaptor; + + @Test + @DisplayName("Happy path when all parameters are provided values") + void testSuccess() throws SQLException { + // given + doReturn(mongoDatabase).when(mongoClient).getDatabase(anyString()); + doReturn(Document.parse("{ok: 1.0, n: 1}")) + .when(mongoDatabase) + .runCommand(eq(clientSession), any(BsonDocument.class)); + + // when && then + try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { + + preparedStatement.setString(1, "War and Peace"); + preparedStatement.setString(2, "Leo Tolstoy"); + preparedStatement.setInt(3, 1869); + preparedStatement.setBoolean(4, false); + preparedStatement.setString(5, "classic"); + + preparedStatement.executeUpdate(); + + verify(mongoDatabase).runCommand(eq(clientSession), commandCaptor.capture()); + var command = commandCaptor.getValue(); + var expectedDoc = BsonDocument.parse( + """ + { + insert: "books", + documents: [ + { + title: "War and Peace", + author: "Leo Tolstoy", + publishYear: 1869, + outOfStock: false, + tags: [ + "classic" + ] + } + ] + } + """); + assertEquals(expectedDoc, command); + } + } + + @Test + @DisplayName("SQLException is thrown when parameterIndex is zero and parameters not empty") + void testParameterIndexUnderflow() { + try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { + var sqlException = + assertThrows(SQLException.class, () -> preparedStatement.setString(0, "War and Peace")); + assertEquals(ERROR_MSG_PARAMETER_INDEX_UNDERFLOW, sqlException.getMessage()); + verify(mongoClient, never()).getDatabase(anyString()); + } + } + + @Test + @DisplayName("SQLException is thrown when parameterIndex surpassing parameter list size") + void testParameterIndexOverflow() { + try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { + var sqlException = + assertThrows(SQLException.class, () -> preparedStatement.setString(6, "War and Peace")); + assertEquals(ERROR_MSG_PARAMETER_INDEX_OVERFLOW, sqlException.getMessage()); + verify(mongoClient, never()).getDatabase(anyString()); + } + } + + @Test + @DisplayName("SQLException is thrown when some parameter was provided values more than once") + void testParameterValueProvidedMoreThanOnce() throws SQLException { + try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { + + preparedStatement.setString(1, "War and Peace"); + preparedStatement.setString(2, "Leo Tolstoy"); + preparedStatement.setBoolean(4, false); + preparedStatement.setString(5, "classic"); + preparedStatement.setInt(3, 1865); + + assertThrows(SQLException.class, () -> preparedStatement.setInt(3, 1869)); + verify(mongoClient, never()).getDatabase(anyString()); + } + } + + @Test + @DisplayName("SQLException is thrown when some parameter is not provided value") + void testParameterNotAllProvided() throws SQLException { + try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { + + preparedStatement.setString(1, "War and Peace"); + preparedStatement.setString(2, "Leo Tolstoy"); + preparedStatement.setInt(3, 1869); + preparedStatement.setBoolean(4, false); + + var sqlException = assertThrows(SQLException.class, preparedStatement::executeUpdate); + assertEquals(ERROR_MSG_PARAMETER_UNRESOLVED, sqlException.getMessage()); + verify(mongoClient, never()).getDatabase(anyString()); + } + } + } + + @Nested + class CloseTests { + + @FunctionalInterface + interface PreparedStatementMethodInvocation { + void run(MongoPreparedStatement pstmt) throws SQLException; + } + + @ParameterizedTest(name = "SQLException is thrown when \"{0}\" is called on a closed MongoPreparedStatement") + @MethodSource("getMongoPreparedStatementMethodInvocationsImpactedByClosing") + void testCheckClosed(String label, PreparedStatementMethodInvocation methodInvocation) { + // given + var mql = + """ + { + insert: "books", + documents: [ + { + title: "War and Peace", + author: "Leo Tolstoy", + outOfStock: false, + values: [ + { $undefined: true } + ] + } + ] + } + """; + + var preparedStatement = createMongoPreparedStatement(mql); + preparedStatement.close(); + + // when && then + var sqlException = assertThrows(SQLException.class, () -> methodInvocation.run(preparedStatement)); + assertEquals("MongoPreparedStatement has been closed", sqlException.getMessage()); + } + + private static Stream getMongoPreparedStatementMethodInvocationsImpactedByClosing() { + return Map.ofEntries( + Map.entry("executeQuery()", MongoPreparedStatement::executeQuery), + Map.entry("executeUpdate()", MongoPreparedStatement::executeUpdate), + Map.entry("setNull(int,int)", pstmt -> pstmt.setNull(1, Types.INTEGER)), + Map.entry("setBoolean(int,boolean)", pstmt -> pstmt.setBoolean(1, true)), + Map.entry("setByte(int,byte)", pstmt -> pstmt.setByte(1, (byte) 10)), + Map.entry("setShort(int,short)", pstmt -> pstmt.setShort(1, (short) 10)), + Map.entry("setInt(int,int)", pstmt -> pstmt.setInt(1, 1)), + Map.entry("setLong(int,long)", pstmt -> pstmt.setLong(1, 1L)), + Map.entry("setFloat(int,float)", pstmt -> pstmt.setFloat(1, 1.0F)), + Map.entry("setDouble(int,double)", pstmt -> pstmt.setDouble(1, 1.0)), + Map.entry("setBigDecimal(int,BigDecimal)", pstmt -> pstmt.setBigDecimal(1, null)), + Map.entry("setString(int,String)", pstmt -> pstmt.setString(1, null)), + Map.entry("setBytes(int,byte[])", pstmt -> pstmt.setBytes(1, null)), + Map.entry("setDate(int,Date)", pstmt -> pstmt.setDate(1, null)), + Map.entry("setTime(int,Time)", pstmt -> pstmt.setTime(1, null)), + Map.entry("setTimestamp(int,Timestamp)", pstmt -> pstmt.setTimestamp(1, null)), + Map.entry( + "setBinaryStream(int,InputStream,int)", pstmt -> pstmt.setBinaryStream(1, null, 0)), + Map.entry("setObject(int,Object,int)", pstmt -> pstmt.setObject(1, null, Types.OTHER)), + Map.entry("addBatch()", MongoPreparedStatement::addBatch), + Map.entry("setBlob(int,Blob)", pstmt -> pstmt.setBlob(1, (Blob) null)), + Map.entry("setClob(int,Clob)", pstmt -> pstmt.setClob(1, (Clob) null)), + Map.entry("setArray(int,Array)", pstmt -> pstmt.setArray(1, null)), + Map.entry("setDate(int,Date,Calendar)", pstmt -> pstmt.setDate(1, null, null)), + Map.entry("setTime(int,Time,Calendar)", pstmt -> pstmt.setTime(1, null, null)), + Map.entry( + "setTimestamp(int,Timestamp,Calendar)", pstmt -> pstmt.setTimestamp(1, null, null)), + Map.entry("setNull(int,Object,int)", pstmt -> pstmt.setNull(1, Types.STRUCT, "BOOK"))) + .entrySet() + .stream() + .map(entry -> Arguments.of(entry.getKey(), entry.getValue())); + } + } +} diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java index b88fa3af..07575778 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java @@ -125,7 +125,7 @@ void testCheckClosed(String label, StatementMethodInvocation methodInvocation) { // when && then var exception = assertThrows(SQLException.class, () -> methodInvocation.runOn(mongoStatement)); - assertEquals("Statement has been closed", exception.getMessage()); + assertEquals("MongoStatement has been closed", exception.getMessage()); } private static Stream getMongoStatementMethodInvocationsImpactedByClosing() { From 68bd2acac3e49acba196a38a92b50f93fe6ba2d2 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 16 Jan 2025 13:49:04 -0500 Subject: [PATCH 02/38] disable errorprone scanning on test code (so we can get rid of annoying nullness warnings as well). --- build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e94313d0..62be3131 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -93,8 +93,6 @@ tasks.withType().configureEach { options.errorprone { disableWarningsInGeneratedCode.set(true) option("NullAway:AnnotatedPackages", "com.mongodb.hibernate") - option("NullAway:ExcludedFieldAnnotations", "org.mockito.Mock") - option("NullAway:ExcludedFieldAnnotations", "org.mockito.InjectMocks") } } tasks.compileJava { @@ -102,6 +100,10 @@ tasks.compileJava { options.errorprone.error("NullAway") } +tasks.compileTestJava { + options.errorprone.isEnabled.set(false) +} + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Dependencies From 88e9d0a223c4b02345f9b66e5f47d0e07d300fe1 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 16 Jan 2025 14:02:45 -0500 Subject: [PATCH 03/38] suppress errorprone warnings regarding the usage of java.util.Date in MongoPreparedStatement for we only use its `getTime()` method --- .../java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 9878f31f..23c73a79 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -233,6 +233,7 @@ public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) setParameter(parameterIndex, BsonNull.VALUE); } + @SuppressWarnings("JavaUtilDate") private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Date date) throws SQLException { setParameter(parameterIndex, date == null ? BsonNull.VALUE : new BsonDateTime(date.getTime())); } From c479bf1b9c0e7ef14f925b34fcbb43045abc7494 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 16 Jan 2025 14:22:42 -0500 Subject: [PATCH 04/38] improve MongoPreparedStatementIntegrationTests by making it more complex --- ...ongoPreparedStatementIntegrationTests.java | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java index e7bbe144..6d8bff5d 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java @@ -94,19 +94,22 @@ void setUp() { _id: 1, title: "War and Peace", author: "Leo Tolstoy", - outOfStock: false + outOfStock: false, + tags: [ "classic", "tolstoy" ] }, { _id: 2, title: "Anna Karenina", author: "Leo Tolstoy", - outOfStock: false + outOfStock: false, + tags: [ "classic", "tolstoy" ] }, { _id: 3, title: "Crime and Punishment", author: "Fyodor Dostoevsky", - outOfStock: false + outOfStock: false, + tags: [ "classic", "Dostoevsky", "literature" ] } ] }"""; @@ -115,7 +118,7 @@ void setUp() { @ValueSource(booleans = {true, false}) void testUpdate(boolean autoCommit) { // given - prepareData(INSERT_MQL); + prepareData(); // when && then var expectedDocs = Set.of( @@ -125,7 +128,8 @@ void testUpdate(boolean autoCommit) { _id: 1, title: "War and Peace", author: "Leo Tolstoy", - outOfStock: true + outOfStock: true, + tags: [ "classic", "tolstoy", "literature" ] }"""), BsonDocument.parse( """ @@ -133,7 +137,8 @@ void testUpdate(boolean autoCommit) { _id: 2, title: "Anna Karenina", author: "Leo Tolstoy", - outOfStock: true + outOfStock: true, + tags: [ "classic", "tolstoy", "literature" ] }"""), BsonDocument.parse( """ @@ -141,27 +146,32 @@ void testUpdate(boolean autoCommit) { _id: 3, title: "Crime and Punishment", author: "Fyodor Dostoevsky", - outOfStock: false + outOfStock: false, + tags: [ "classic", "Dostoevsky", "literature" ] }""")); Function pstmtProvider = connection -> { try { var pstmt = (MongoPreparedStatement) connection.prepareStatement( """ - { - update: "books", - updates: [ - { - q: { author: { $undefined: true } }, - u: { - $set: { outOfStock: { $undefined: true } } - }, - multi: true - } - ] - }"""); + { + update: "books", + updates: [ + { + q: { author: { $undefined: true } }, + u: { + $set: { + outOfStock: { $undefined: true } + }, + $push: { tags: { $undefined: true } } + }, + multi: true + } + ] + }"""); pstmt.setString(1, "Leo Tolstoy"); pstmt.setBoolean(2, true); + pstmt.setString(3, "literature"); return pstmt; } catch (SQLException e) { throw new RuntimeException(e); @@ -170,24 +180,24 @@ void testUpdate(boolean autoCommit) { assertExecuteUpdate(pstmtProvider, autoCommit, 2, expectedDocs); } - private void prepareData(String mql) { + private void prepareData() { assertNotNull(session).doWork(connection -> { connection.setAutoCommit(true); var statement = connection.createStatement(); - statement.executeUpdate(mql); + statement.executeUpdate(INSERT_MQL); }); } private void assertExecuteUpdate( Function pstmtProvider, boolean autoCommit, - int expectedRowCount, + int expectedUpdatedRowCount, Set expectedDocuments) { assertNotNull(session).doWork(connection -> { connection.setAutoCommit(autoCommit); var pstmt = pstmtProvider.apply(connection); try { - assertEquals(expectedRowCount, pstmt.executeUpdate()); + assertEquals(expectedUpdatedRowCount, pstmt.executeUpdate()); } finally { if (!autoCommit) { connection.commit(); From 4ea1aaefd91996649994723cda60a5d9db094fe1 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 16 Jan 2025 17:09:36 -0500 Subject: [PATCH 05/38] simplify logic after references to other JDBC implementation --- .../jdbc/MongoPreparedStatement.java | 49 ++++------------- .../jdbc/MongoPreparedStatementTests.java | 54 ++----------------- 2 files changed, 16 insertions(+), 87 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 23c73a79..8d70a840 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -16,6 +16,8 @@ package com.mongodb.hibernate.jdbc; +import static java.lang.String.format; + import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.hibernate.internal.NotYetImplementedException; @@ -62,11 +64,7 @@ final class MongoPreparedStatement extends MongoStatement implements PreparedSta private final BsonDocument command; - // once parameter is provided, the corresponding entry will be set to null - // so duplicated parameter setting could be detected - private final List<@Nullable Consumer> parameters; - - private int unresolvedParameterCount; + private final List> parameters; public MongoPreparedStatement( MongoClient mongoClient, ClientSession clientSession, MongoConnection mongoConnection, String mql) { @@ -74,20 +72,17 @@ public MongoPreparedStatement( this.command = BsonDocument.parse(mql); this.parameters = new ArrayList<>(); parseParameters(command, parameters); - unresolvedParameterCount = parameters.size(); } @Override public ResultSet executeQuery() throws SQLException { checkClosed(); - ensureAllParametersResolved(); throw new NotYetImplementedException(); } @Override public int executeUpdate() throws SQLException { checkClosed(); - ensureAllParametersResolved(); return executeUpdate(command); } @@ -240,22 +235,15 @@ private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Da private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { checkClosed(); - if (parameterIndex <= 0) { - throw new SQLException(ERROR_MSG_PARAMETER_INDEX_UNDERFLOW); - } - if (parameterIndex > parameters.size()) { - throw new SQLException(ERROR_MSG_PARAMETER_INDEX_OVERFLOW); + if (parameterIndex < 1 || parameterIndex > parameters.size()) { + throw new SQLException( + format(ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID, parameterIndex, parameters.size())); } var parameterValueConsumer = parameters.get(parameterIndex - 1); - if (parameterValueConsumer == null) { - throw new SQLException(ERROR_MSG_PARAMETER_VALUE_SET_MORE_THAN_ONCE); - } parameterValueConsumer.accept(parameterValue); - parameters.set(parameterIndex - 1, null); - unresolvedParameterCount--; } - private static void parseParameters(BsonDocument command, List<@Nullable Consumer> parameters) { + private static void parseParameters(BsonDocument command, List> parameters) { for (var entry : command.entrySet()) { if (isParameterMarker(entry.getValue())) { parameters.add(entry::setValue); @@ -265,7 +253,7 @@ private static void parseParameters(BsonDocument command, List<@Nullable Consume } } - private static void parseParameters(BsonArray array, List<@Nullable Consumer> parameters) { + private static void parseParameters(BsonArray array, List> parameters) { IntStream.range(0, array.size()).forEach(i -> { var value = array.get(i); if (isParameterMarker(value)) { @@ -276,7 +264,7 @@ private static void parseParameters(BsonArray array, List<@Nullable Consumer> parameters) { + private static void parseParameters(BsonValue value, List> parameters) { if (value.isDocument()) { parseParameters(value.asDocument(), parameters); } else if (value.isArray()) { @@ -288,27 +276,12 @@ private static boolean isParameterMarker(BsonValue value) { return value.getBsonType() == BsonType.UNDEFINED; } - private void ensureAllParametersResolved() throws SQLException { - if (unresolvedParameterCount > 0) { - throw new SQLException("Unresolved parameter found"); - } - } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) List<@Nullable Consumer> getParameters() { return parameters; } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - static final String ERROR_MSG_PARAMETER_INDEX_UNDERFLOW = "Parameter index should start from 1"; - - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - static final String ERROR_MSG_PARAMETER_INDEX_OVERFLOW = - "Parameter index should not be larger than parameters size"; - - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - static final String ERROR_MSG_PARAMETER_VALUE_SET_MORE_THAN_ONCE = "Parameter index has been set previously"; - - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - static final String ERROR_MSG_PARAMETER_UNRESOLVED = "Unresolved parameter found"; + static final String ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID = + "Parameter index invalid: %d; should be within [1, %d]"; } diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 7febbde5..1b4559d1 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -16,9 +16,8 @@ package com.mongodb.hibernate.jdbc; -import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PARAMETER_INDEX_OVERFLOW; -import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PARAMETER_INDEX_UNDERFLOW; -import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PARAMETER_UNRESOLVED; +import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -250,55 +249,12 @@ void testSuccess() throws SQLException { } @Test - @DisplayName("SQLException is thrown when parameterIndex is zero and parameters not empty") - void testParameterIndexUnderflow() { + @DisplayName("SQLException is thrown when parameter index is invalid") + void testParameterIndexInvalid() { try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { var sqlException = assertThrows(SQLException.class, () -> preparedStatement.setString(0, "War and Peace")); - assertEquals(ERROR_MSG_PARAMETER_INDEX_UNDERFLOW, sqlException.getMessage()); - verify(mongoClient, never()).getDatabase(anyString()); - } - } - - @Test - @DisplayName("SQLException is thrown when parameterIndex surpassing parameter list size") - void testParameterIndexOverflow() { - try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { - var sqlException = - assertThrows(SQLException.class, () -> preparedStatement.setString(6, "War and Peace")); - assertEquals(ERROR_MSG_PARAMETER_INDEX_OVERFLOW, sqlException.getMessage()); - verify(mongoClient, never()).getDatabase(anyString()); - } - } - - @Test - @DisplayName("SQLException is thrown when some parameter was provided values more than once") - void testParameterValueProvidedMoreThanOnce() throws SQLException { - try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { - - preparedStatement.setString(1, "War and Peace"); - preparedStatement.setString(2, "Leo Tolstoy"); - preparedStatement.setBoolean(4, false); - preparedStatement.setString(5, "classic"); - preparedStatement.setInt(3, 1865); - - assertThrows(SQLException.class, () -> preparedStatement.setInt(3, 1869)); - verify(mongoClient, never()).getDatabase(anyString()); - } - } - - @Test - @DisplayName("SQLException is thrown when some parameter is not provided value") - void testParameterNotAllProvided() throws SQLException { - try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { - - preparedStatement.setString(1, "War and Peace"); - preparedStatement.setString(2, "Leo Tolstoy"); - preparedStatement.setInt(3, 1869); - preparedStatement.setBoolean(4, false); - - var sqlException = assertThrows(SQLException.class, preparedStatement::executeUpdate); - assertEquals(ERROR_MSG_PARAMETER_UNRESOLVED, sqlException.getMessage()); + assertEquals(format(ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID, 0, 5), sqlException.getMessage()); verify(mongoClient, never()).getDatabase(anyString()); } } From 5c63de46eecda4135081ee05b014cddddc84c3d7 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 08:44:18 -0500 Subject: [PATCH 06/38] method renaming to improve readability --- .../jdbc/MongoPreparedStatement.java | 24 +++++++++---------- .../hibernate/jdbc/MongoStatement.java | 9 ++----- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 8d70a840..e70514d6 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -32,7 +32,6 @@ import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; -import java.sql.Types; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -71,7 +70,7 @@ public MongoPreparedStatement( super(mongoClient, clientSession, mongoConnection); this.command = BsonDocument.parse(mql); this.parameters = new ArrayList<>(); - parseParameters(command, parameters); + dispatchContainerParsing(command, parameters); } @Override @@ -83,7 +82,7 @@ public ResultSet executeQuery() throws SQLException { @Override public int executeUpdate() throws SQLException { checkClosed(); - return executeUpdate(command); + return executeUpdateCommand(command); } @Override @@ -169,12 +168,13 @@ public void setBinaryStream(int parameterIndex, @Nullable InputStream x, int len @Override public void setObject(int parameterIndex, @Nullable Object x, int targetSqlType) throws SQLException { checkClosed(); - throw new NotYetImplementedException(); + throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } @Override public void setObject(int parameterIndex, @Nullable Object x) throws SQLException { - setObject(parameterIndex, x, Types.OTHER); + checkClosed(); + throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } // --------------------------JDBC 2.0----------------------------- @@ -243,32 +243,32 @@ private void setParameter(int parameterIndex, BsonValue parameterValue) throws S parameterValueConsumer.accept(parameterValue); } - private static void parseParameters(BsonDocument command, List> parameters) { + private static void dispatchContainerParsing(BsonDocument command, List> parameters) { for (var entry : command.entrySet()) { if (isParameterMarker(entry.getValue())) { parameters.add(entry::setValue); } else if (entry.getValue().getBsonType().isContainer()) { - parseParameters(entry.getValue(), parameters); + dispatchContainerParsing(entry.getValue(), parameters); } } } - private static void parseParameters(BsonArray array, List> parameters) { + private static void dispatchContainerParsing(BsonArray array, List> parameters) { IntStream.range(0, array.size()).forEach(i -> { var value = array.get(i); if (isParameterMarker(value)) { parameters.add(v -> array.set(i, v)); } else if (value.getBsonType().isContainer()) { - parseParameters(value, parameters); + dispatchContainerParsing(value, parameters); } }); } - private static void parseParameters(BsonValue value, List> parameters) { + private static void dispatchContainerParsing(BsonValue value, List> parameters) { if (value.isDocument()) { - parseParameters(value.asDocument(), parameters); + dispatchContainerParsing(value.asDocument(), parameters); } else if (value.isArray()) { - parseParameters(value.asArray(), parameters); + dispatchContainerParsing(value.asArray(), parameters); } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index 306e0410..019c609a 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -64,10 +64,10 @@ public ResultSet executeQuery(String mql) throws SQLException { public int executeUpdate(String mql) throws SQLException { checkClosed(); var command = parse(mql); - return executeUpdate(command); + return executeUpdateCommand(command); } - protected int executeUpdate(BsonDocument command) throws SQLException { + protected int executeUpdateCommand(BsonDocument command) throws SQLException { startTransactionIfNeeded(); try { return mongoClient @@ -226,11 +226,6 @@ private static BsonDocument parse(String mql) throws SQLSyntaxErrorException { /** * Starts transaction for the first {@link java.sql.Statement} executing if * {@linkplain MongoConnection#getAutoCommit() auto-commit} is disabled. - * - * @see #executeQuery(String) - * @see #executeUpdate(String) - * @see #execute(String) - * @see #executeBatch() */ private void startTransactionIfNeeded() throws SQLException { if (!mongoConnection.getAutoCommit() && !clientSession.hasActiveTransaction()) { From 6a1bf555a2e233bfa9473e2a35e6059e8500c526 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 14:22:17 -0500 Subject: [PATCH 07/38] remove 'protected' visibility keyword usage on MongoStatement#executeUpdateCommand --- src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index 019c609a..effbc095 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -67,7 +67,7 @@ public int executeUpdate(String mql) throws SQLException { return executeUpdateCommand(command); } - protected int executeUpdateCommand(BsonDocument command) throws SQLException { + int executeUpdateCommand(BsonDocument command) throws SQLException { startTransactionIfNeeded(); try { return mongoClient @@ -75,7 +75,7 @@ protected int executeUpdateCommand(BsonDocument command) throws SQLException { .runCommand(clientSession, command) .getInteger("n"); } catch (Exception e) { - throw new SQLException("Failed to run #executeUpdate(String)", e); + throw new SQLException("Failed to execute update command", e); } } From bc32a4ccc80c168d1e0ab07e6fdc1d0292b0eeb6 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 14:36:33 -0500 Subject: [PATCH 08/38] made all JDBC adapters as interfaces --- .../hibernate/jdbc/ConnectionAdapter.java | 112 +++++++++--------- .../hibernate/jdbc/MongoConnection.java | 4 +- .../hibernate/jdbc/MongoStatement.java | 4 +- .../jdbc/PreparedStatementAdapter.java | 2 +- .../hibernate/jdbc/StatementAdapter.java | 92 +++++++------- 5 files changed, 107 insertions(+), 107 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/ConnectionAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/ConnectionAdapter.java index ada9a776..e23751bb 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/ConnectionAdapter.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/ConnectionAdapter.java @@ -39,283 +39,283 @@ import org.jspecify.annotations.Nullable; /** - * A {@link java.sql.Connection} implementation class that throws exceptions for all its API methods. + * A {@link java.sql.Connection} adapter interface that throws exceptions for all its API methods. * * @see MongoConnection */ -abstract class ConnectionAdapter implements Connection { +interface ConnectionAdapter extends Connection { @Override - public Statement createStatement() throws SQLException { + default Statement createStatement() throws SQLException { throw new SQLFeatureNotSupportedException("createStatement not implemented"); } @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { + default PreparedStatement prepareStatement(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("prepareStatement not implemented"); } @Override - public CallableStatement prepareCall(String sql) throws SQLException { + default CallableStatement prepareCall(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("prepareCall not implemented"); } @Override - public String nativeSQL(String sql) throws SQLException { + default String nativeSQL(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("nativeSQL not implemented"); } @Override - public void setAutoCommit(boolean autoCommit) throws SQLException { + default void setAutoCommit(boolean autoCommit) throws SQLException { throw new SQLFeatureNotSupportedException("setAutoCommit not implemented"); } @Override - public boolean getAutoCommit() throws SQLException { + default boolean getAutoCommit() throws SQLException { throw new SQLFeatureNotSupportedException("getAutoCommit not implemented"); } @Override - public void commit() throws SQLException { + default void commit() throws SQLException { throw new SQLFeatureNotSupportedException("commit not implemented"); } @Override - public void rollback() throws SQLException { + default void rollback() throws SQLException { throw new SQLFeatureNotSupportedException("rollback not implemented"); } @Override - public void close() throws SQLException { + default void close() throws SQLException { throw new SQLFeatureNotSupportedException("close not implemented"); } @Override - public boolean isClosed() throws SQLException { + default boolean isClosed() throws SQLException { throw new SQLFeatureNotSupportedException("isClosed not implemented"); } @Override - public DatabaseMetaData getMetaData() throws SQLException { + default DatabaseMetaData getMetaData() throws SQLException { throw new SQLFeatureNotSupportedException("geetMetaData not implemented"); } @Override - public void setReadOnly(boolean readOnly) throws SQLException { + default void setReadOnly(boolean readOnly) throws SQLException { throw new SQLFeatureNotSupportedException("setReadOnly not implemented"); } @Override - public boolean isReadOnly() throws SQLException { + default boolean isReadOnly() throws SQLException { throw new SQLFeatureNotSupportedException("isReadOnly not implemented"); } @Override - public void setCatalog(String catalog) throws SQLException { + default void setCatalog(String catalog) throws SQLException { throw new SQLFeatureNotSupportedException("setCatalog not implemented"); } @Override - public @Nullable String getCatalog() throws SQLException { + default @Nullable String getCatalog() throws SQLException { throw new SQLFeatureNotSupportedException("getCatalog not implemented"); } @Override - public void setTransactionIsolation(int level) throws SQLException { + default void setTransactionIsolation(int level) throws SQLException { throw new SQLFeatureNotSupportedException("setTransactionIsolation not implemented"); } @Override - public int getTransactionIsolation() throws SQLException { + default int getTransactionIsolation() throws SQLException { throw new SQLFeatureNotSupportedException("getTransactionIsolation not implemented"); } @Override - public @Nullable SQLWarning getWarnings() throws SQLException { + default @Nullable SQLWarning getWarnings() throws SQLException { throw new SQLFeatureNotSupportedException("getWarnings not implemented"); } @Override - public void clearWarnings() throws SQLException { + default void clearWarnings() throws SQLException { throw new SQLFeatureNotSupportedException("clearWarnings not implemented"); } @Override - public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + default Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { throw new SQLFeatureNotSupportedException("createStatement not implemented"); } @Override - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + default PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { throw new SQLFeatureNotSupportedException("prepareStatement not implemented"); } @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + default CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { throw new SQLFeatureNotSupportedException("prepareCall not implemented"); } @Override - public Map> getTypeMap() throws SQLException { + default Map> getTypeMap() throws SQLException { throw new SQLFeatureNotSupportedException("getTypeMap not implemented"); } @Override - public void setTypeMap(Map> map) throws SQLException { + default void setTypeMap(Map> map) throws SQLException { throw new SQLFeatureNotSupportedException("setTypeMap not implemented"); } @Override - public void setHoldability(int holdability) throws SQLException { + default void setHoldability(int holdability) throws SQLException { throw new SQLFeatureNotSupportedException("setHoldability not implemented"); } @Override - public int getHoldability() throws SQLException { + default int getHoldability() throws SQLException { throw new SQLFeatureNotSupportedException("getHoldability not implemented"); } @Override - public Savepoint setSavepoint() throws SQLException { + default Savepoint setSavepoint() throws SQLException { throw new SQLFeatureNotSupportedException("setSavepoint not implemented"); } @Override - public Savepoint setSavepoint(String name) throws SQLException { + default Savepoint setSavepoint(String name) throws SQLException { throw new SQLFeatureNotSupportedException("setSavepoint not implemented"); } @Override - public void rollback(Savepoint savepoint) throws SQLException { + default void rollback(Savepoint savepoint) throws SQLException { throw new SQLFeatureNotSupportedException("rollback not implemented"); } @Override - public void releaseSavepoint(Savepoint savepoint) throws SQLException { + default void releaseSavepoint(Savepoint savepoint) throws SQLException { throw new SQLFeatureNotSupportedException("releaseSavepoint not implemented"); } @Override - public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + default Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { throw new SQLFeatureNotSupportedException("createStatement not implemented"); } @Override - public PreparedStatement prepareStatement( + default PreparedStatement prepareStatement( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { throw new SQLFeatureNotSupportedException("prepareStatement not implemented"); } @Override - public CallableStatement prepareCall( + default CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { throw new SQLFeatureNotSupportedException("prepareCall not implemented"); } @Override - public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + default PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { throw new SQLFeatureNotSupportedException("prepareStatement not implemented"); } @Override - public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + default PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { throw new SQLFeatureNotSupportedException("prepareStatement not implemented"); } @Override - public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + default PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { throw new SQLFeatureNotSupportedException("prepareStatement not implemented"); } @Override - public Clob createClob() throws SQLException { + default Clob createClob() throws SQLException { throw new SQLFeatureNotSupportedException("createClob not implemented"); } @Override - public Blob createBlob() throws SQLException { + default Blob createBlob() throws SQLException { throw new SQLFeatureNotSupportedException("createBlob not implemented"); } @Override - public NClob createNClob() throws SQLException { + default NClob createNClob() throws SQLException { throw new SQLFeatureNotSupportedException("createNClob not implemented"); } @Override - public SQLXML createSQLXML() throws SQLException { + default SQLXML createSQLXML() throws SQLException { throw new SQLFeatureNotSupportedException("createSQLXML not implemented"); } @Override - public boolean isValid(int timeout) throws SQLException { + default boolean isValid(int timeout) throws SQLException { throw new SQLFeatureNotSupportedException("isValid not implemented"); } @Override - public void setClientInfo(String name, String value) throws SQLClientInfoException { + default void setClientInfo(String name, String value) throws SQLClientInfoException { throw new SQLClientInfoException("setClientInfo not implemented", Collections.emptyMap()); } @Override - public void setClientInfo(Properties properties) throws SQLClientInfoException { + default void setClientInfo(Properties properties) throws SQLClientInfoException { throw new SQLClientInfoException("setClientInfo not implemented", Collections.emptyMap()); } @Override - public String getClientInfo(String name) throws SQLException { + default String getClientInfo(String name) throws SQLException { throw new SQLFeatureNotSupportedException("getClientInfo not implemented"); } @Override - public Properties getClientInfo() throws SQLException { + default Properties getClientInfo() throws SQLException { throw new SQLFeatureNotSupportedException("getClientInfo not implemented"); } @Override - public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + default Array createArrayOf(String typeName, Object[] elements) throws SQLException { throw new SQLFeatureNotSupportedException("createArrayOf not implemented"); } @Override - public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + default Struct createStruct(String typeName, Object[] attributes) throws SQLException { throw new SQLFeatureNotSupportedException("createStruct not implemented"); } @Override - public void setSchema(String schema) throws SQLException { + default void setSchema(String schema) throws SQLException { throw new SQLFeatureNotSupportedException("setSchema not implemented"); } @Override - public @Nullable String getSchema() throws SQLException { + default @Nullable String getSchema() throws SQLException { throw new SQLFeatureNotSupportedException("getSchema not implemented"); } @Override - public void abort(Executor executor) throws SQLException { + default void abort(Executor executor) throws SQLException { throw new SQLFeatureNotSupportedException("abort not implemented"); } @Override - public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + default void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { throw new SQLFeatureNotSupportedException("setNetworkTimeout not implemented"); } @Override - public int getNetworkTimeout() throws SQLException { + default int getNetworkTimeout() throws SQLException { throw new SQLFeatureNotSupportedException("getNetworkTimeout not implemented"); } @Override - public T unwrap(Class iface) throws SQLException { + default T unwrap(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException("unwrap not implemented"); } @Override - public boolean isWrapperFor(Class iface) throws SQLException { + default boolean isWrapperFor(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException("isWrapperFor not implemented"); } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java index 0c869fef..785a6093 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java @@ -36,9 +36,9 @@ * MongoDB Dialect's JDBC {@linkplain java.sql.Connection connection} implementation class. * *

It only focuses on API methods Mongo Dialect will support. All the other methods are implemented by throwing - * exceptions in its parent class. + * exceptions in its parent {@linkplain ConnectionAdapter adapter interface}. */ -final class MongoConnection extends ConnectionAdapter { +final class MongoConnection implements ConnectionAdapter { // temporary hard-coded database prior to the db config tech design finalizing public static final String DATABASE = "mongo-hibernate-test"; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index effbc095..e6eedc1f 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -37,9 +37,9 @@ * MongoDB Dialect's JDBC {@link java.sql.Statement} implementation class. * *

It only focuses on API methods Mongo Dialect will support. All the other methods are implemented by throwing - * exceptions in its parent class. + * exceptions in its parent {@link StatementAdapter adapter interface}. */ -class MongoStatement extends StatementAdapter { +class MongoStatement implements StatementAdapter { private final MongoClient mongoClient; private final MongoConnection mongoConnection; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java index 13767c86..af871267 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java @@ -43,7 +43,7 @@ * * @see MongoPreparedStatement */ -interface PreparedStatementAdapter extends PreparedStatement { +interface PreparedStatementAdapter extends StatementAdapter, PreparedStatement { @Override default ResultSet executeQuery() throws SQLException { throw new SQLFeatureNotSupportedException("executeQuery not implemented"); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/StatementAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/StatementAdapter.java index b634c6cb..f6759d06 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/StatementAdapter.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/StatementAdapter.java @@ -25,228 +25,228 @@ import org.jspecify.annotations.Nullable; /** - * A {@link java.sql.Statement} implementation class that throws exceptions for all its API methods. + * A {@link java.sql.Statement} adapter interface that throws exceptions for all its API methods. * * @see MongoStatement */ -abstract class StatementAdapter implements Statement { +interface StatementAdapter extends Statement { @Override - public ResultSet executeQuery(String sql) throws SQLException { + default ResultSet executeQuery(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("executeQuery not implemented"); } @Override - public int executeUpdate(String sql) throws SQLException { + default int executeUpdate(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("executeUpdate not implemented"); } @Override - public void close() throws SQLException { + default void close() throws SQLException { throw new SQLFeatureNotSupportedException("close not implemented"); } @Override - public int getMaxFieldSize() throws SQLException { + default int getMaxFieldSize() throws SQLException { throw new SQLFeatureNotSupportedException("getMaxFieldSize not implemented"); } @Override - public void setMaxFieldSize(int max) throws SQLException { + default void setMaxFieldSize(int max) throws SQLException { throw new SQLFeatureNotSupportedException("setMaxFieldSize not implemented"); } @Override - public int getMaxRows() throws SQLException { + default int getMaxRows() throws SQLException { throw new SQLFeatureNotSupportedException("getMaxRows not implemented"); } @Override - public void setMaxRows(int max) throws SQLException { + default void setMaxRows(int max) throws SQLException { throw new SQLFeatureNotSupportedException("setMaxRows not implemented"); } @Override - public void setEscapeProcessing(boolean enable) throws SQLException { + default void setEscapeProcessing(boolean enable) throws SQLException { throw new SQLFeatureNotSupportedException("setEscapeProcessing not implemented"); } @Override - public int getQueryTimeout() throws SQLException { + default int getQueryTimeout() throws SQLException { throw new SQLFeatureNotSupportedException("getQueryTimeout not implemented"); } @Override - public void setQueryTimeout(int seconds) throws SQLException { + default void setQueryTimeout(int seconds) throws SQLException { throw new SQLFeatureNotSupportedException("setQueryTimeout not implemented"); } @Override - public void cancel() throws SQLException { + default void cancel() throws SQLException { throw new SQLFeatureNotSupportedException("cancel not implemented"); } @Override - public @Nullable SQLWarning getWarnings() throws SQLException { + default @Nullable SQLWarning getWarnings() throws SQLException { throw new SQLFeatureNotSupportedException("getWarnings not implemented"); } @Override - public void clearWarnings() throws SQLException { + default void clearWarnings() throws SQLException { throw new SQLFeatureNotSupportedException("clearWarnings not implemented"); } @Override - public void setCursorName(String name) throws SQLException { + default void setCursorName(String name) throws SQLException { throw new SQLFeatureNotSupportedException("setCursorName not implemented"); } @Override - public boolean execute(String sql) throws SQLException { + default boolean execute(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("execute not implemented"); } @Override - public @Nullable ResultSet getResultSet() throws SQLException { + default @Nullable ResultSet getResultSet() throws SQLException { throw new SQLFeatureNotSupportedException("getResultSet not implemented"); } @Override - public int getUpdateCount() throws SQLException { + default int getUpdateCount() throws SQLException { throw new SQLFeatureNotSupportedException("getUpdateCount not implemented"); } @Override - public boolean getMoreResults() throws SQLException { + default boolean getMoreResults() throws SQLException { throw new SQLFeatureNotSupportedException("getMoreResults not implemented"); } @Override - public void setFetchDirection(int direction) throws SQLException { + default void setFetchDirection(int direction) throws SQLException { throw new SQLFeatureNotSupportedException("setFetchDirection not implemented"); } @Override - public int getFetchDirection() throws SQLException { + default int getFetchDirection() throws SQLException { throw new SQLFeatureNotSupportedException("getFetchDirection not implemented"); } @Override - public void setFetchSize(int rows) throws SQLException { + default void setFetchSize(int rows) throws SQLException { throw new SQLFeatureNotSupportedException("setFetchSize not implemented"); } @Override - public int getFetchSize() throws SQLException { + default int getFetchSize() throws SQLException { throw new SQLFeatureNotSupportedException("getFetchSize not implemented"); } @Override - public int getResultSetConcurrency() throws SQLException { + default int getResultSetConcurrency() throws SQLException { throw new SQLFeatureNotSupportedException("getResultSetConcurrency not implemented"); } @Override - public int getResultSetType() throws SQLException { + default int getResultSetType() throws SQLException { throw new SQLFeatureNotSupportedException("getResultSetType not implemented"); } @Override - public void addBatch(String sql) throws SQLException { + default void addBatch(String sql) throws SQLException { throw new SQLFeatureNotSupportedException("addBatch not implemented"); } @Override - public void clearBatch() throws SQLException { + default void clearBatch() throws SQLException { throw new SQLFeatureNotSupportedException("clearBatch not implemented"); } @Override - public int[] executeBatch() throws SQLException { + default int[] executeBatch() throws SQLException { throw new SQLFeatureNotSupportedException("executeBatch not implemented"); } @Override - public Connection getConnection() throws SQLException { + default Connection getConnection() throws SQLException { throw new SQLFeatureNotSupportedException("getConnection not implemented"); } @Override - public boolean getMoreResults(int current) throws SQLException { + default boolean getMoreResults(int current) throws SQLException { throw new SQLFeatureNotSupportedException("getMoreResults not implemented"); } @Override - public ResultSet getGeneratedKeys() throws SQLException { + default ResultSet getGeneratedKeys() throws SQLException { throw new SQLFeatureNotSupportedException("getGeneratedKeys not implemented"); } @Override - public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + default int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { throw new SQLFeatureNotSupportedException("executeUpdate not implemented"); } @Override - public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + default int executeUpdate(String sql, int[] columnIndexes) throws SQLException { throw new SQLFeatureNotSupportedException("executeUpdate not implemented"); } @Override - public int executeUpdate(String sql, String[] columnNames) throws SQLException { + default int executeUpdate(String sql, String[] columnNames) throws SQLException { throw new SQLFeatureNotSupportedException("executeUpdate not implemented"); } @Override - public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + default boolean execute(String sql, int autoGeneratedKeys) throws SQLException { throw new SQLFeatureNotSupportedException("execute not implemented"); } @Override - public boolean execute(String sql, int[] columnIndexes) throws SQLException { + default boolean execute(String sql, int[] columnIndexes) throws SQLException { throw new SQLFeatureNotSupportedException("execute not implemented"); } @Override - public boolean execute(String sql, String[] columnNames) throws SQLException { + default boolean execute(String sql, String[] columnNames) throws SQLException { throw new SQLFeatureNotSupportedException("execute not implemented"); } @Override - public int getResultSetHoldability() throws SQLException { + default int getResultSetHoldability() throws SQLException { throw new SQLFeatureNotSupportedException("getResultSetHoldability not implemented"); } @Override - public boolean isClosed() throws SQLException { + default boolean isClosed() throws SQLException { throw new SQLFeatureNotSupportedException("isClosed not implemented"); } @Override - public void setPoolable(boolean poolable) throws SQLException { + default void setPoolable(boolean poolable) throws SQLException { throw new SQLFeatureNotSupportedException("setPoolable not implemented"); } @Override - public boolean isPoolable() throws SQLException { + default boolean isPoolable() throws SQLException { throw new SQLFeatureNotSupportedException("isPoolable not implemented"); } @Override - public void closeOnCompletion() throws SQLException { + default void closeOnCompletion() throws SQLException { throw new SQLFeatureNotSupportedException("closeOnCompletion not implemented"); } @Override - public boolean isCloseOnCompletion() throws SQLException { + default boolean isCloseOnCompletion() throws SQLException { throw new SQLFeatureNotSupportedException("isCloseOnCompletion not implemented"); } @Override - public T unwrap(Class iface) throws SQLException { + default T unwrap(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException("unwrap not implemented"); } @Override - public boolean isWrapperFor(Class iface) throws SQLException { + default boolean isWrapperFor(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException("isWrapperFor not implemented"); } } From 43e5a2bcedc15702b8b0b19f0b9cfd94629e8092 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 15:18:26 -0500 Subject: [PATCH 09/38] add checkSqlTypeSupported() method to MongoPreparedStatement --- .../jdbc/MongoPreparedStatement.java | 29 +++++++++++++++++++ .../jdbc/MongoPreparedStatementTests.java | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index e70514d6..6e1f7468 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -28,10 +28,13 @@ import java.sql.Blob; import java.sql.Clob; import java.sql.Date; +import java.sql.JDBCType; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Time; import java.sql.Timestamp; +import java.sql.Types; import java.util.ArrayList; import java.util.Calendar; import java.util.List; @@ -88,6 +91,7 @@ public int executeUpdate() throws SQLException { @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { checkClosed(); + checkSqlTypeSupported(sqlType); setParameter(parameterIndex, BsonNull.VALUE); } @@ -225,6 +229,7 @@ public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Ca @Override public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) throws SQLException { checkClosed(); + checkSqlTypeSupported(sqlType); setParameter(parameterIndex, BsonNull.VALUE); } @@ -284,4 +289,28 @@ private static boolean isParameterMarker(BsonValue value) { @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) static final String ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID = "Parameter index invalid: %d; should be within [1, %d]"; + + private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedException { + switch (sqlType) { + case Types.BOOLEAN: + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.BIGINT: + case Types.REAL: + case Types.DOUBLE: + case Types.NUMERIC: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.BINARY: + case Types.LONGVARBINARY: + case Types.DATE: + case Types.TIME: + case Types.TIMESTAMP: + break; + default: + throw new SQLFeatureNotSupportedException( + "Unsupported sql type: " + JDBCType.valueOf(sqlType).getName()); + } + } } diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 1b4559d1..096497a1 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -326,7 +326,7 @@ private static Stream getMongoPreparedStatementMethodInvocationsImpac Map.entry("setTime(int,Time,Calendar)", pstmt -> pstmt.setTime(1, null, null)), Map.entry( "setTimestamp(int,Timestamp,Calendar)", pstmt -> pstmt.setTimestamp(1, null, null)), - Map.entry("setNull(int,Object,int)", pstmt -> pstmt.setNull(1, Types.STRUCT, "BOOK"))) + Map.entry("setNull(int,Object,String)", pstmt -> pstmt.setNull(1, Types.STRUCT, "BOOK"))) .entrySet() .stream() .map(entry -> Arguments.of(entry.getKey(), entry.getValue())); From 62c76a0376a889b58a22711cfac6bc8957a06b13 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 15:23:39 -0500 Subject: [PATCH 10/38] add fail() in MongoPreparedStatement#parseParameters(BsonValue,List) --- .../hibernate/internal/MongoAssertions.java | 12 ++++++++++++ .../jdbc/MongoPreparedStatement.java | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java b/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java index d50173ab..16085edb 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java @@ -41,4 +41,16 @@ public static T assertNotNull(@Nullable T value) throws AssertionError { } return value; } + + /** + * Asserts that failure happens invariably. + * + * @param msg The failure message. + * @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail("failure message")}. + * This may be helpful in non-{@code void} methods. + * @throws AssertionError Always + */ + public static AssertionError fail(String msg) throws AssertionError { + throw new AssertionError(assertNotNull(msg)); + } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 6e1f7468..73aacd3c 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -16,6 +16,7 @@ package com.mongodb.hibernate.jdbc; +import static com.mongodb.assertions.Assertions.fail; import static java.lang.String.format; import com.mongodb.client.ClientSession; @@ -73,7 +74,7 @@ public MongoPreparedStatement( super(mongoClient, clientSession, mongoConnection); this.command = BsonDocument.parse(mql); this.parameters = new ArrayList<>(); - dispatchContainerParsing(command, parameters); + parseParameters(command, parameters); } @Override @@ -248,32 +249,34 @@ private void setParameter(int parameterIndex, BsonValue parameterValue) throws S parameterValueConsumer.accept(parameterValue); } - private static void dispatchContainerParsing(BsonDocument command, List> parameters) { + private static void parseParameters(BsonDocument command, List> parameters) { for (var entry : command.entrySet()) { if (isParameterMarker(entry.getValue())) { parameters.add(entry::setValue); } else if (entry.getValue().getBsonType().isContainer()) { - dispatchContainerParsing(entry.getValue(), parameters); + parseParameters(entry.getValue(), parameters); } } } - private static void dispatchContainerParsing(BsonArray array, List> parameters) { + private static void parseParameters(BsonArray array, List> parameters) { IntStream.range(0, array.size()).forEach(i -> { var value = array.get(i); if (isParameterMarker(value)) { parameters.add(v -> array.set(i, v)); } else if (value.getBsonType().isContainer()) { - dispatchContainerParsing(value, parameters); + parseParameters(value, parameters); + } else { + fail(); } }); } - private static void dispatchContainerParsing(BsonValue value, List> parameters) { + private static void parseParameters(BsonValue value, List> parameters) { if (value.isDocument()) { - dispatchContainerParsing(value.asDocument(), parameters); + parseParameters(value.asDocument(), parameters); } else if (value.isArray()) { - dispatchContainerParsing(value.asArray(), parameters); + parseParameters(value.asArray(), parameters); } } From a34dbf2678a619109c323002af3b7ad4c6cdbeb7 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 15:27:59 -0500 Subject: [PATCH 11/38] rename parameters to parameterValueSetters --- .../hibernate/internal/MongoAssertions.java | 4 ++-- .../hibernate/jdbc/MongoPreparedStatement.java | 18 +++++++++--------- .../jdbc/MongoPreparedStatementTests.java | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java b/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java index 16085edb..e4235225 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java @@ -46,8 +46,8 @@ public static T assertNotNull(@Nullable T value) throws AssertionError { * Asserts that failure happens invariably. * * @param msg The failure message. - * @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw fail("failure message")}. - * This may be helpful in non-{@code void} methods. + * @return Never completes normally. The return type is {@link AssertionError} to allow writing {@code throw + * fail("failure message")}. This may be helpful in non-{@code void} methods. * @throws AssertionError Always */ public static AssertionError fail(String msg) throws AssertionError { diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 73aacd3c..4b46e9f9 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -67,14 +67,14 @@ final class MongoPreparedStatement extends MongoStatement implements PreparedSta private final BsonDocument command; - private final List> parameters; + private final List> parameterValueSetters; public MongoPreparedStatement( MongoClient mongoClient, ClientSession clientSession, MongoConnection mongoConnection, String mql) { super(mongoClient, clientSession, mongoConnection); this.command = BsonDocument.parse(mql); - this.parameters = new ArrayList<>(); - parseParameters(command, parameters); + this.parameterValueSetters = new ArrayList<>(); + parseParameters(command, parameterValueSetters); } @Override @@ -241,12 +241,12 @@ private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Da private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { checkClosed(); - if (parameterIndex < 1 || parameterIndex > parameters.size()) { + if (parameterIndex < 1 || parameterIndex > parameterValueSetters.size()) { throw new SQLException( - format(ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID, parameterIndex, parameters.size())); + format(ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID, parameterIndex, parameterValueSetters.size())); } - var parameterValueConsumer = parameters.get(parameterIndex - 1); - parameterValueConsumer.accept(parameterValue); + var parameterValueSetter = parameterValueSetters.get(parameterIndex - 1); + parameterValueSetter.accept(parameterValue); } private static void parseParameters(BsonDocument command, List> parameters) { @@ -285,8 +285,8 @@ private static boolean isParameterMarker(BsonValue value) { } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - List<@Nullable Consumer> getParameters() { - return parameters; + List<@Nullable Consumer> getParameterValueSetters() { + return parameterValueSetters; } @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 096497a1..24048f83 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -94,7 +94,7 @@ void testZeroParameters() { // when && then try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertTrue(preparedStatement.getParameters().isEmpty()); + assertTrue(preparedStatement.getParameterValueSetters().isEmpty()); } } @@ -118,7 +118,7 @@ void testParameterMarkerInDocumentContainer() { // when && then try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertEquals(1, preparedStatement.getParameters().size()); + assertEquals(1, preparedStatement.getParameterValueSetters().size()); } } @@ -146,7 +146,7 @@ void testParameterMarkerInArrayContainer() { // when && then try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertEquals(2, preparedStatement.getParameters().size()); + assertEquals(2, preparedStatement.getParameterValueSetters().size()); } } @@ -173,7 +173,7 @@ void testParameterMarkerInBothDocumentAndArrayContainers() { // when && then try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertEquals(3, preparedStatement.getParameters().size()); + assertEquals(3, preparedStatement.getParameterValueSetters().size()); } } } From 82b6194f5851dca25cceb68e3bc46592d931695f Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 15:35:53 -0500 Subject: [PATCH 12/38] fix the wrong usag of fail() in MongoPreparedStatement --- .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 4b46e9f9..4817f6d8 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -266,8 +266,6 @@ private static void parseParameters(BsonArray array, List> p parameters.add(v -> array.set(i, v)); } else if (value.getBsonType().isContainer()) { parseParameters(value, parameters); - } else { - fail(); } }); } @@ -277,6 +275,8 @@ private static void parseParameters(BsonValue value, List> p parseParameters(value.asDocument(), parameters); } else if (value.isArray()) { parseParameters(value.asArray(), parameters); + } else { + fail(); } } From b19d26d0b89e88a6f462795ab456e3c1843f9ca9 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 16:00:49 -0500 Subject: [PATCH 13/38] get rid of stream usage in MongoPreparedStatement --- .../mongodb/hibernate/jdbc/MongoPreparedStatement.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 4817f6d8..7f3cddd4 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -40,7 +40,6 @@ import java.util.Calendar; import java.util.List; import java.util.function.Consumer; -import java.util.stream.IntStream; import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonBoolean; @@ -260,14 +259,15 @@ private static void parseParameters(BsonDocument command, List> parameters) { - IntStream.range(0, array.size()).forEach(i -> { + for (var i = 0; i < array.size(); i++) { var value = array.get(i); if (isParameterMarker(value)) { - parameters.add(v -> array.set(i, v)); + var idx = i; + parameters.add(v -> array.set(idx, v)); } else if (value.getBsonType().isContainer()) { parseParameters(value, parameters); } - }); + } } private static void parseParameters(BsonValue value, List> parameters) { From 203d05f3bfbf2a0271fd2b1a21b25e6211c1b9dc Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 16:07:20 -0500 Subject: [PATCH 14/38] remove exception message format constant usage in MongoPreparedStatement --- .../mongodb/hibernate/jdbc/MongoPreparedStatement.java | 9 +++------ .../hibernate/jdbc/MongoPreparedStatementTests.java | 5 +++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 7f3cddd4..7c4f7eb9 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -241,8 +241,9 @@ private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Da private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { checkClosed(); if (parameterIndex < 1 || parameterIndex > parameterValueSetters.size()) { - throw new SQLException( - format(ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID, parameterIndex, parameterValueSetters.size())); + throw new SQLException(format( + "Parameter index invalid: %d; should be within [1, %d]", + parameterIndex, parameterValueSetters.size())); } var parameterValueSetter = parameterValueSetters.get(parameterIndex - 1); parameterValueSetter.accept(parameterValue); @@ -289,10 +290,6 @@ private static boolean isParameterMarker(BsonValue value) { return parameterValueSetters; } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) - static final String ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID = - "Parameter index invalid: %d; should be within [1, %d]"; - private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedException { switch (sqlType) { case Types.BOOLEAN: diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 24048f83..345a382d 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -16,7 +16,6 @@ package com.mongodb.hibernate.jdbc; -import static com.mongodb.hibernate.jdbc.MongoPreparedStatement.ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID; import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -254,7 +253,9 @@ void testParameterIndexInvalid() { try (var preparedStatement = createMongoPreparedStatement(EXAMPLE_MQL)) { var sqlException = assertThrows(SQLException.class, () -> preparedStatement.setString(0, "War and Peace")); - assertEquals(format(ERROR_MSG_PATTERN_PARAMETER_INDEX_INVALID, 0, 5), sqlException.getMessage()); + assertEquals( + format("Parameter index invalid: %d; should be within [1, %d]", 0, 5), + sqlException.getMessage()); verify(mongoClient, never()).getDatabase(anyString()); } } From b978c8ec5a63551e30c347916ce9e47e820d06c6 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 16:10:40 -0500 Subject: [PATCH 15/38] use static import for VisibleForTesting in MongoPreparedStatement --- .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 7c4f7eb9..fa4e0628 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -17,6 +17,7 @@ package com.mongodb.hibernate.jdbc; import static com.mongodb.assertions.Assertions.fail; +import static com.mongodb.hibernate.internal.VisibleForTesting.AccessModifier.PRIVATE; import static java.lang.String.format; import com.mongodb.client.ClientSession; @@ -285,7 +286,7 @@ private static boolean isParameterMarker(BsonValue value) { return value.getBsonType() == BsonType.UNDEFINED; } - @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + @VisibleForTesting(otherwise = PRIVATE) List<@Nullable Consumer> getParameterValueSetters() { return parameterValueSetters; } From 75db3d9c49a8e455b67f8a3799e41fa488b1c5b8 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 16:14:22 -0500 Subject: [PATCH 16/38] move checkClosed to public methods instead of common private method --- .../hibernate/jdbc/MongoPreparedStatement.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index fa4e0628..e0a783f1 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -98,66 +98,79 @@ public void setNull(int parameterIndex, int sqlType) throws SQLException { @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { + checkClosed(); setParameter(parameterIndex, BsonBoolean.valueOf(x)); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { + checkClosed(); setInt(parameterIndex, x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { + checkClosed(); setInt(parameterIndex, x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { + checkClosed(); setParameter(parameterIndex, new BsonInt32(x)); } @Override public void setLong(int parameterIndex, long x) throws SQLException { + checkClosed(); setParameter(parameterIndex, new BsonInt64(x)); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { + checkClosed(); setDouble(parameterIndex, x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { + checkClosed(); setParameter(parameterIndex, new BsonDouble(x)); } @Override public void setBigDecimal(int parameterIndex, @Nullable BigDecimal x) throws SQLException { + checkClosed(); setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonDecimal128(new Decimal128(x))); } @Override public void setString(int parameterIndex, @Nullable String x) throws SQLException { + checkClosed(); setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonString(x)); } @Override public void setBytes(int parameterIndex, byte @Nullable [] x) throws SQLException { + checkClosed(); setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonBinary(x)); } @Override public void setDate(int parameterIndex, @Nullable Date x) throws SQLException { + checkClosed(); setBsonDateTimeParameter(parameterIndex, x); } @Override public void setTime(int parameterIndex, @Nullable Time x) throws SQLException { + checkClosed(); setBsonDateTimeParameter(parameterIndex, x); } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x) throws SQLException { + checkClosed(); setBsonDateTimeParameter(parameterIndex, x); } @@ -240,7 +253,6 @@ private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Da } private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { - checkClosed(); if (parameterIndex < 1 || parameterIndex > parameterValueSetters.size()) { throw new SQLException(format( "Parameter index invalid: %d; should be within [1, %d]", From 17b91acd2ba593a50d9753e019c833031baf7d4e Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 16:22:24 -0500 Subject: [PATCH 17/38] make null value setting go through centralized setNull() method --- .../jdbc/MongoPreparedStatement.java | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index e0a783f1..5537fa54 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -141,37 +141,49 @@ public void setDouble(int parameterIndex, double x) throws SQLException { @Override public void setBigDecimal(int parameterIndex, @Nullable BigDecimal x) throws SQLException { checkClosed(); - setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonDecimal128(new Decimal128(x))); + if (x == null) { + setNull(parameterIndex, Types.NUMERIC); + } else { + setParameter(parameterIndex, new BsonDecimal128(new Decimal128(x))); + } } @Override public void setString(int parameterIndex, @Nullable String x) throws SQLException { checkClosed(); - setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonString(x)); + if (x == null) { + setNull(parameterIndex, Types.VARCHAR); + } else { + setParameter(parameterIndex, new BsonString(x)); + } } @Override public void setBytes(int parameterIndex, byte @Nullable [] x) throws SQLException { checkClosed(); - setParameter(parameterIndex, x == null ? BsonNull.VALUE : new BsonBinary(x)); + if (x == null) { + setNull(parameterIndex, Types.VARBINARY); + } else { + setParameter(parameterIndex, new BsonBinary(x)); + } } @Override public void setDate(int parameterIndex, @Nullable Date x) throws SQLException { checkClosed(); - setBsonDateTimeParameter(parameterIndex, x); + setBsonDateTimeParameter(parameterIndex, x, Types.DATE); } @Override public void setTime(int parameterIndex, @Nullable Time x) throws SQLException { checkClosed(); - setBsonDateTimeParameter(parameterIndex, x); + setBsonDateTimeParameter(parameterIndex, x, Types.TIME); } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x) throws SQLException { checkClosed(); - setBsonDateTimeParameter(parameterIndex, x); + setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); } @Override @@ -248,8 +260,13 @@ public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) } @SuppressWarnings("JavaUtilDate") - private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Date date) throws SQLException { - setParameter(parameterIndex, date == null ? BsonNull.VALUE : new BsonDateTime(date.getTime())); + private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Date date, int sqlType) + throws SQLException { + if (date == null) { + setNull(parameterIndex, sqlType); + } else { + setParameter(parameterIndex, new BsonDateTime(date.getTime())); + } } private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { From 2e45866d02ed73cf42a356c64fe8a8bc57963431 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 16:27:57 -0500 Subject: [PATCH 18/38] make time setter invoke other overloaded methods accepting nullable Calendar --- .../jdbc/MongoPreparedStatement.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 5537fa54..0d156d40 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -170,20 +170,17 @@ public void setBytes(int parameterIndex, byte @Nullable [] x) throws SQLExceptio @Override public void setDate(int parameterIndex, @Nullable Date x) throws SQLException { - checkClosed(); - setBsonDateTimeParameter(parameterIndex, x, Types.DATE); + setDate(parameterIndex, x, null); } @Override public void setTime(int parameterIndex, @Nullable Time x) throws SQLException { - checkClosed(); - setBsonDateTimeParameter(parameterIndex, x, Types.TIME); + setTime(parameterIndex, x, null); } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x) throws SQLException { - checkClosed(); - setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); + setTimestamp(parameterIndex, x, null); } @Override @@ -237,19 +234,31 @@ public void setArray(int parameterIndex, @Nullable Array x) throws SQLException @Override public void setDate(int parameterIndex, @Nullable Date x, @Nullable Calendar cal) throws SQLException { checkClosed(); - throw new NotYetImplementedException(); + if (cal == null) { + setBsonDateTimeParameter(parameterIndex, x, Types.DATE); + } else { + throw new NotYetImplementedException(); + } } @Override public void setTime(int parameterIndex, @Nullable Time x, @Nullable Calendar cal) throws SQLException { checkClosed(); - throw new NotYetImplementedException(); + if (cal == null) { + setBsonDateTimeParameter(parameterIndex, x, Types.TIME); + } else { + throw new NotYetImplementedException(); + } } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Calendar cal) throws SQLException { checkClosed(); - throw new NotYetImplementedException(); + if (cal == null) { + setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); + } else { + throw new NotYetImplementedException(); + } } @Override From 62862f5bc7350d4fb0a6b223f4b61555e54414e7 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 17:56:23 -0500 Subject: [PATCH 19/38] Update src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java Co-authored-by: Valentin Kovalenko --- ...ongoPreparedStatementIntegrationTests.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java index 6d8bff5d..29408061 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementIntegrationTests.java @@ -195,19 +195,20 @@ private void assertExecuteUpdate( Set expectedDocuments) { assertNotNull(session).doWork(connection -> { connection.setAutoCommit(autoCommit); - var pstmt = pstmtProvider.apply(connection); - try { - assertEquals(expectedUpdatedRowCount, pstmt.executeUpdate()); - } finally { - if (!autoCommit) { - connection.commit(); + try (var pstmt = pstmtProvider.apply(connection)) { + try { + assertEquals(expectedUpdatedRowCount, pstmt.executeUpdate()); + } finally { + if (!autoCommit) { + connection.commit(); + } } + var realDocuments = pstmt.getMongoDatabase() + .getCollection("books", BsonDocument.class) + .find() + .into(new HashSet<>()); + assertEquals(expectedDocuments, realDocuments); } - var realDocuments = pstmt.getMongoDatabase() - .getCollection("books", BsonDocument.class) - .find() - .into(new HashSet<>()); - assertEquals(expectedDocuments, realDocuments); }); } } From d4f691ab00d1cb6029111e44417b7e57738a4845 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 18:00:00 -0500 Subject: [PATCH 20/38] remove unnecessary @MockitoSettings(strictness = Strictness.WARN) in MongoPreparedStatementTests --- .../jdbc/MongoStatementIntegrationTests.java | 24 +++++++++---------- .../jdbc/MongoPreparedStatementTests.java | 3 --- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoStatementIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoStatementIntegrationTests.java index 0da7fbf7..c3e870c0 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoStatementIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/jdbc/MongoStatementIntegrationTests.java @@ -238,20 +238,20 @@ private void assertExecuteUpdate( String mql, boolean autoCommit, int expectedRowCount, Set expectedDocuments) { assertNotNull(session).doWork(connection -> { connection.setAutoCommit(autoCommit); - var statement = (MongoStatement) connection.createStatement(); - try { - assertEquals(expectedRowCount, statement.executeUpdate(mql)); - } finally { - if (!autoCommit) { - connection.commit(); + try (var stmt = (MongoStatement) connection.createStatement()) { + try { + assertEquals(expectedRowCount, stmt.executeUpdate(mql)); + } finally { + if (!autoCommit) { + connection.commit(); + } } + var realDocuments = stmt.getMongoDatabase() + .getCollection("books", BsonDocument.class) + .find() + .into(new HashSet<>()); + assertEquals(expectedDocuments, realDocuments); } - var realDocuments = statement - .getMongoDatabase() - .getCollection("books", BsonDocument.class) - .find() - .into(new HashSet<>()); - assertEquals(expectedDocuments, realDocuments); }); } } diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 345a382d..ed88515c 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -50,11 +50,8 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; @ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.WARN) class MongoPreparedStatementTests { @Mock(answer = RETURNS_SMART_NULLS) From 17dd554ba3dc2dd80a7f6c065d587fee9336489e Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 20 Jan 2025 18:02:58 -0500 Subject: [PATCH 21/38] remove unnecessary ParameterParsingTests in MongoPreparedStatementTests --- .../jdbc/MongoPreparedStatement.java | 7 -- .../jdbc/MongoPreparedStatementTests.java | 108 ------------------ 2 files changed, 115 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 0d156d40..7896e854 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -17,13 +17,11 @@ package com.mongodb.hibernate.jdbc; import static com.mongodb.assertions.Assertions.fail; -import static com.mongodb.hibernate.internal.VisibleForTesting.AccessModifier.PRIVATE; import static java.lang.String.format; import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.hibernate.internal.NotYetImplementedException; -import com.mongodb.hibernate.internal.VisibleForTesting; import java.io.InputStream; import java.math.BigDecimal; import java.sql.Array; @@ -324,11 +322,6 @@ private static boolean isParameterMarker(BsonValue value) { return value.getBsonType() == BsonType.UNDEFINED; } - @VisibleForTesting(otherwise = PRIVATE) - List<@Nullable Consumer> getParameterValueSetters() { - return parameterValueSetters; - } - private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedException { switch (sqlType) { case Types.BOOLEAN: diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index ed88515c..13f29965 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -19,7 +19,6 @@ import static java.lang.String.format; 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.Answers.RETURNS_SMART_NULLS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -67,113 +66,6 @@ private MongoPreparedStatement createMongoPreparedStatement(String mql) { return new MongoPreparedStatement(mongoClient, clientSession, mongoConnection, mql); } - @Nested - class ParameterParsingTests { - - @Test - @DisplayName("Empty parameters should be created if no parameter marker found") - void testZeroParameters() { - // given - var mql = - """ - { - insert: "books", - documents: [ - { - title: "War and Peace", - author: "Leo Tolstoy", - outOfStock: false - } - ] - } - """; - - // when && then - try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertTrue(preparedStatement.getParameterValueSetters().isEmpty()); - } - } - - @Test - @DisplayName("Parameter marker could be found in embedded document field") - void testParameterMarkerInDocumentContainer() { - // given - var mql = - """ - { - insert: "books", - documents: [ - { - title: { $undefined: true }, - author: "Leo Tolstoy", - outOfStock: false - } - ] - } - """; - - // when && then - try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertEquals(1, preparedStatement.getParameterValueSetters().size()); - } - } - - @Test - @DisplayName("Parameter marker could be found in embedded array field") - void testParameterMarkerInArrayContainer() { - // given - var mql = - """ - { - insert: "books", - documents: [ - { - title: "War and Peace", - author: "Leo Tolstoy", - outOfStock: false, - tags: [ - { $undefined: true }, - { $undefined: true }, - ] - } - ] - } - """; - - // when && then - try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertEquals(2, preparedStatement.getParameterValueSetters().size()); - } - } - - @Test - @DisplayName("Parameter marker could be found in both document and array embedded fields") - void testParameterMarkerInBothDocumentAndArrayContainers() { - // given - var mql = - """ - { - insert: "books", - documents: [ - { - title: { $undefined: true }, - author: { $undefined: true }, - outOfStock: false, - tags: [ - { $undefined: true } - ] - } - ] - } - """; - - // when && then - try (var preparedStatement = createMongoPreparedStatement(mql)) { - assertEquals(3, preparedStatement.getParameterValueSetters().size()); - } - } - } - @Nested class ParameterValueSettingTests { From 46ecaaf264d66b6392208e200e0c5e479fd50e6c Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 21 Jan 2025 10:46:37 -0500 Subject: [PATCH 22/38] fix erroneous usage of fail in MongoPreparedStatement --- .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 7896e854..f89ad971 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -16,7 +16,7 @@ package com.mongodb.hibernate.jdbc; -import static com.mongodb.assertions.Assertions.fail; +import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static java.lang.String.format; import com.mongodb.client.ClientSession; @@ -314,7 +314,7 @@ private static void parseParameters(BsonValue value, List> p } else if (value.isArray()) { parseParameters(value.asArray(), parameters); } else { - fail(); + fail("Only BSON container type (BsonDocument or BsonArray) is accepted"); } } From 5609f0fba1681abe5fc4b73dfc8739014cd37d57 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 21 Jan 2025 10:50:34 -0500 Subject: [PATCH 23/38] rename `parameters` method arguments consistently in MongoPreparedStatement --- .../hibernate/jdbc/MongoPreparedStatement.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index f89ad971..19801460 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -286,33 +286,33 @@ private void setParameter(int parameterIndex, BsonValue parameterValue) throws S parameterValueSetter.accept(parameterValue); } - private static void parseParameters(BsonDocument command, List> parameters) { + private static void parseParameters(BsonDocument command, List> parameterValueSetters) { for (var entry : command.entrySet()) { if (isParameterMarker(entry.getValue())) { - parameters.add(entry::setValue); + parameterValueSetters.add(entry::setValue); } else if (entry.getValue().getBsonType().isContainer()) { - parseParameters(entry.getValue(), parameters); + parseParameters(entry.getValue(), parameterValueSetters); } } } - private static void parseParameters(BsonArray array, List> parameters) { + private static void parseParameters(BsonArray array, List> parameterValueSetters) { for (var i = 0; i < array.size(); i++) { var value = array.get(i); if (isParameterMarker(value)) { var idx = i; - parameters.add(v -> array.set(idx, v)); + parameterValueSetters.add(v -> array.set(idx, v)); } else if (value.getBsonType().isContainer()) { - parseParameters(value, parameters); + parseParameters(value, parameterValueSetters); } } } - private static void parseParameters(BsonValue value, List> parameters) { + private static void parseParameters(BsonValue value, List> parameterValueSetters) { if (value.isDocument()) { - parseParameters(value.asDocument(), parameters); + parseParameters(value.asDocument(), parameterValueSetters); } else if (value.isArray()) { - parseParameters(value.asArray(), parameters); + parseParameters(value.asArray(), parameterValueSetters); } else { fail("Only BSON container type (BsonDocument or BsonArray) is accepted"); } From d3a10bd4295975757625577249c67fdb47331815 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 21 Jan 2025 15:02:30 -0500 Subject: [PATCH 24/38] add checking explicitly at the beginning of each public method regardless of whther it forwards to another overloaded method or not --- .../jdbc/MongoPreparedStatement.java | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 19801460..55d1148e 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -90,6 +90,7 @@ public int executeUpdate() throws SQLException { @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); checkSqlTypeSupported(sqlType); setParameter(parameterIndex, BsonNull.VALUE); } @@ -97,48 +98,56 @@ public void setNull(int parameterIndex, int sqlType) throws SQLException { @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setParameter(parameterIndex, BsonBoolean.valueOf(x)); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setInt(parameterIndex, x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setInt(parameterIndex, x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setParameter(parameterIndex, new BsonInt32(x)); } @Override public void setLong(int parameterIndex, long x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setParameter(parameterIndex, new BsonInt64(x)); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setDouble(parameterIndex, x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); setParameter(parameterIndex, new BsonDouble(x)); } @Override public void setBigDecimal(int parameterIndex, @Nullable BigDecimal x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); if (x == null) { setNull(parameterIndex, Types.NUMERIC); } else { @@ -149,6 +158,7 @@ public void setBigDecimal(int parameterIndex, @Nullable BigDecimal x) throws SQL @Override public void setString(int parameterIndex, @Nullable String x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); if (x == null) { setNull(parameterIndex, Types.VARCHAR); } else { @@ -159,6 +169,7 @@ public void setString(int parameterIndex, @Nullable String x) throws SQLExceptio @Override public void setBytes(int parameterIndex, byte @Nullable [] x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); if (x == null) { setNull(parameterIndex, Types.VARBINARY); } else { @@ -168,22 +179,29 @@ public void setBytes(int parameterIndex, byte @Nullable [] x) throws SQLExceptio @Override public void setDate(int parameterIndex, @Nullable Date x) throws SQLException { + checkClosed(); + checkParameterIndex(parameterIndex); setDate(parameterIndex, x, null); } @Override public void setTime(int parameterIndex, @Nullable Time x) throws SQLException { + checkClosed(); + checkParameterIndex(parameterIndex); setTime(parameterIndex, x, null); } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x) throws SQLException { + checkClosed(); + checkParameterIndex(parameterIndex); setTimestamp(parameterIndex, x, null); } @Override public void setBinaryStream(int parameterIndex, @Nullable InputStream x, int length) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); throw new NotYetImplementedException(); } @@ -193,12 +211,14 @@ public void setBinaryStream(int parameterIndex, @Nullable InputStream x, int len @Override public void setObject(int parameterIndex, @Nullable Object x, int targetSqlType) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } @Override public void setObject(int parameterIndex, @Nullable Object x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } @@ -214,24 +234,28 @@ public void addBatch() throws SQLException { @Override public void setBlob(int parameterIndex, @Nullable Blob x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); throw new NotYetImplementedException(); } @Override public void setClob(int parameterIndex, @Nullable Clob x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); throw new NotYetImplementedException(); } @Override public void setArray(int parameterIndex, @Nullable Array x) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); throw new NotYetImplementedException(); } @Override public void setDate(int parameterIndex, @Nullable Date x, @Nullable Calendar cal) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); if (cal == null) { setBsonDateTimeParameter(parameterIndex, x, Types.DATE); } else { @@ -242,6 +266,7 @@ public void setDate(int parameterIndex, @Nullable Date x, @Nullable Calendar cal @Override public void setTime(int parameterIndex, @Nullable Time x, @Nullable Calendar cal) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); if (cal == null) { setBsonDateTimeParameter(parameterIndex, x, Types.TIME); } else { @@ -252,6 +277,7 @@ public void setTime(int parameterIndex, @Nullable Time x, @Nullable Calendar cal @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Calendar cal) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); if (cal == null) { setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); } else { @@ -262,6 +288,7 @@ public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Ca @Override public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) throws SQLException { checkClosed(); + checkParameterIndex(parameterIndex); checkSqlTypeSupported(sqlType); setParameter(parameterIndex, BsonNull.VALUE); } @@ -276,12 +303,7 @@ private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Da } } - private void setParameter(int parameterIndex, BsonValue parameterValue) throws SQLException { - if (parameterIndex < 1 || parameterIndex > parameterValueSetters.size()) { - throw new SQLException(format( - "Parameter index invalid: %d; should be within [1, %d]", - parameterIndex, parameterValueSetters.size())); - } + private void setParameter(int parameterIndex, BsonValue parameterValue) { var parameterValueSetter = parameterValueSetters.get(parameterIndex - 1); parameterValueSetter.accept(parameterValue); } @@ -345,4 +367,12 @@ private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedExc "Unsupported sql type: " + JDBCType.valueOf(sqlType).getName()); } } + + private void checkParameterIndex(int parameterIndex) throws SQLException { + if (parameterIndex < 1 || parameterIndex > parameterValueSetters.size()) { + throw new SQLException(format( + "Parameter index invalid: %d; should be within [1, %d]", + parameterIndex, parameterValueSetters.size())); + } + } } From 9ed673654f1e829cd608c260ad3dd73315998650 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 21 Jan 2025 15:15:48 -0500 Subject: [PATCH 25/38] implement set time methods accepting Calendar parameter --- .../hibernate/jdbc/MongoPreparedStatement.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 55d1148e..42c3e162 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -256,33 +256,21 @@ public void setArray(int parameterIndex, @Nullable Array x) throws SQLException public void setDate(int parameterIndex, @Nullable Date x, @Nullable Calendar cal) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - if (cal == null) { - setBsonDateTimeParameter(parameterIndex, x, Types.DATE); - } else { - throw new NotYetImplementedException(); - } + setBsonDateTimeParameter(parameterIndex, x, Types.DATE); } @Override public void setTime(int parameterIndex, @Nullable Time x, @Nullable Calendar cal) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - if (cal == null) { - setBsonDateTimeParameter(parameterIndex, x, Types.TIME); - } else { - throw new NotYetImplementedException(); - } + setBsonDateTimeParameter(parameterIndex, x, Types.TIME); } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Calendar cal) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - if (cal == null) { - setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); - } else { - throw new NotYetImplementedException(); - } + setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); } @Override From d93dfdcfb69a26f29068e5c2ee51fd1a1cc21f5e Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 21 Jan 2025 15:52:02 -0500 Subject: [PATCH 26/38] change setNull() for user-typed overloaded method to throw NotYetImplementedException --- .../mongodb/hibernate/jdbc/MongoPreparedStatement.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 42c3e162..fd1e7dc9 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -277,8 +277,7 @@ public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Ca public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - checkSqlTypeSupported(sqlType); - setParameter(parameterIndex, BsonNull.VALUE); + throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } @SuppressWarnings("JavaUtilDate") @@ -334,9 +333,6 @@ private static boolean isParameterMarker(BsonValue value) { private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedException { switch (sqlType) { - case Types.BOOLEAN: - case Types.TINYINT: - case Types.SMALLINT: case Types.INTEGER: case Types.BIGINT: case Types.REAL: @@ -349,6 +345,8 @@ private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedExc case Types.DATE: case Types.TIME: case Types.TIMESTAMP: + case Types.TIME_WITH_TIMEZONE: + case Types.TIMESTAMP_WITH_TIMEZONE: break; default: throw new SQLFeatureNotSupportedException( From 08916c15b6a90cbcbbd75257d873905a0ed20a07 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Wed, 22 Jan 2025 09:28:52 -0500 Subject: [PATCH 27/38] refactor setNull() as per Javadoc; improve checkParameterIndex() logic --- .../jdbc/MongoPreparedStatement.java | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index fd1e7dc9..f2a89d63 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -91,7 +91,15 @@ public int executeUpdate() throws SQLException { public void setNull(int parameterIndex, int sqlType) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - checkSqlTypeSupported(sqlType); + switch (sqlType) { + case Types.DATALINK: + case Types.JAVA_OBJECT: + case Types.REF: + case Types.ROWID: + case Types.SQLXML: + throw new SQLFeatureNotSupportedException( + "Unsupported sql type: " + JDBCType.valueOf(sqlType).getName()); + } setParameter(parameterIndex, BsonNull.VALUE); } @@ -331,30 +339,10 @@ private static boolean isParameterMarker(BsonValue value) { return value.getBsonType() == BsonType.UNDEFINED; } - private void checkSqlTypeSupported(int sqlType) throws SQLFeatureNotSupportedException { - switch (sqlType) { - case Types.INTEGER: - case Types.BIGINT: - case Types.REAL: - case Types.DOUBLE: - case Types.NUMERIC: - case Types.VARCHAR: - case Types.LONGVARCHAR: - case Types.BINARY: - case Types.LONGVARBINARY: - case Types.DATE: - case Types.TIME: - case Types.TIMESTAMP: - case Types.TIME_WITH_TIMEZONE: - case Types.TIMESTAMP_WITH_TIMEZONE: - break; - default: - throw new SQLFeatureNotSupportedException( - "Unsupported sql type: " + JDBCType.valueOf(sqlType).getName()); - } - } - private void checkParameterIndex(int parameterIndex) throws SQLException { + if (parameterValueSetters.isEmpty()) { + throw new SQLException("No parameter exists"); + } if (parameterIndex < 1 || parameterIndex > parameterValueSetters.size()) { throw new SQLException(format( "Parameter index invalid: %d; should be within [1, %d]", From 96692d698b369a88df3747fba64c8fee156e4b0f Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Wed, 22 Jan 2025 09:43:02 -0500 Subject: [PATCH 28/38] refactor setNull() as per Javadoc; improve checkParameterIndex() logic --- .../mongodb/hibernate/jdbc/MongoPreparedStatement.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index f2a89d63..39f626ba 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -92,11 +92,19 @@ public void setNull(int parameterIndex, int sqlType) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); switch (sqlType) { + case Types.ARRAY: + case Types.BLOB: + case Types.CLOB: case Types.DATALINK: case Types.JAVA_OBJECT: + case Types.NCHAR: + case Types.NCLOB: + case Types.NVARCHAR: + case Types.LONGNVARCHAR: case Types.REF: case Types.ROWID: case Types.SQLXML: + case Types.STRUCT: throw new SQLFeatureNotSupportedException( "Unsupported sql type: " + JDBCType.valueOf(sqlType).getName()); } From 939424b937b1b2ff3decfd979e2cdc1ee5662f23 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Wed, 22 Jan 2025 10:07:43 -0500 Subject: [PATCH 29/38] use global configuration to simplify mock answer smart nullness config --- .../hibernate/jdbc/MongoConnectionTests.java | 3 +- .../jdbc/MongoPreparedStatementTests.java | 6 ++-- .../hibernate/jdbc/MongoStatementTests.java | 6 ++-- .../configuration/MockitoConfiguration.java | 32 +++++++++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/mockito/configuration/MockitoConfiguration.java diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java index 9f930f14..2c5dda28 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Answers.RETURNS_SMART_NULLS; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; @@ -45,7 +44,7 @@ @ExtendWith(MockitoExtension.class) class MongoConnectionTests { - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private ClientSession clientSession; @InjectMocks diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 13f29965..06dc6421 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -53,13 +53,13 @@ @ExtendWith(MockitoExtension.class) class MongoPreparedStatementTests { - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private MongoClient mongoClient; - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private ClientSession clientSession; - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private MongoConnection mongoConnection; private MongoPreparedStatement createMongoPreparedStatement(String mql) { diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java index 07575778..b5d6efd9 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java @@ -49,13 +49,13 @@ @ExtendWith(MockitoExtension.class) class MongoStatementTests { - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private MongoClient mongoClient; - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private ClientSession clientSession; - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private MongoConnection mongoConnection; @InjectMocks diff --git a/src/test/java/org/mockito/configuration/MockitoConfiguration.java b/src/test/java/org/mockito/configuration/MockitoConfiguration.java new file mode 100644 index 00000000..e68b9019 --- /dev/null +++ b/src/test/java/org/mockito/configuration/MockitoConfiguration.java @@ -0,0 +1,32 @@ +/* + * 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 org.mockito.configuration; + +import org.mockito.Answers; +import org.mockito.stubbing.Answer; + +/** + * Mockito's global configuration overriding mechanism. Before the issue is resolved, this seems the best way to configure + * {@link Answers#RETURNS_SMART_NULLS RETURNS_SMART_NULLS} as the default Mock {@link Answer}. + */ +public class MockitoConfiguration extends DefaultMockitoConfiguration { + + public Answer getDefaultAnswer() { + return Answers.RETURNS_SMART_NULLS; + } +} From cc10c1babebccc7e9d8870adfd2fa1be4bf01791 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 23 Jan 2025 09:55:35 -0500 Subject: [PATCH 30/38] remove Blob and Clob support --- .../hibernate/jdbc/MongoConnection.java | 28 ------------------- .../jdbc/MongoPreparedStatement.java | 16 ----------- .../jdbc/MongoPreparedStatementTests.java | 4 --- 3 files changed, 48 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java index 785a6093..16e64528 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java @@ -20,14 +20,10 @@ import com.mongodb.client.MongoClient; import com.mongodb.hibernate.internal.NotYetImplementedException; import java.sql.Array; -import java.sql.Blob; -import java.sql.Clob; import java.sql.DatabaseMetaData; -import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.SQLWarning; -import java.sql.SQLXML; import java.sql.Statement; import java.sql.Struct; import org.jspecify.annotations.Nullable; @@ -157,30 +153,6 @@ public PreparedStatement prepareStatement(String mql, int resultSetType, int res // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SQL99 data types - @Override - public Clob createClob() throws SQLException { - checkClosed(); - throw new NotYetImplementedException(); - } - - @Override - public Blob createBlob() throws SQLException { - checkClosed(); - throw new NotYetImplementedException(); - } - - @Override - public NClob createNClob() throws SQLException { - checkClosed(); - throw new NotYetImplementedException(); - } - - @Override - public SQLXML createSQLXML() throws SQLException { - checkClosed(); - throw new NotYetImplementedException(); - } - @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { checkClosed(); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 39f626ba..4e22e4e0 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -25,8 +25,6 @@ import java.io.InputStream; import java.math.BigDecimal; import java.sql.Array; -import java.sql.Blob; -import java.sql.Clob; import java.sql.Date; import java.sql.JDBCType; import java.sql.ResultSet; @@ -247,20 +245,6 @@ public void addBatch() throws SQLException { "To be implemented in scope of https://jira.mongodb.org/browse/HIBERNATE-35"); } - @Override - public void setBlob(int parameterIndex, @Nullable Blob x) throws SQLException { - checkClosed(); - checkParameterIndex(parameterIndex); - throw new NotYetImplementedException(); - } - - @Override - public void setClob(int parameterIndex, @Nullable Clob x) throws SQLException { - checkClosed(); - checkParameterIndex(parameterIndex); - throw new NotYetImplementedException(); - } - @Override public void setArray(int parameterIndex, @Nullable Array x) throws SQLException { checkClosed(); diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 06dc6421..2e866d6d 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -30,8 +30,6 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoDatabase; -import java.sql.Blob; -import java.sql.Clob; import java.sql.SQLException; import java.sql.Types; import java.util.Map; @@ -209,8 +207,6 @@ private static Stream getMongoPreparedStatementMethodInvocationsImpac "setBinaryStream(int,InputStream,int)", pstmt -> pstmt.setBinaryStream(1, null, 0)), Map.entry("setObject(int,Object,int)", pstmt -> pstmt.setObject(1, null, Types.OTHER)), Map.entry("addBatch()", MongoPreparedStatement::addBatch), - Map.entry("setBlob(int,Blob)", pstmt -> pstmt.setBlob(1, (Blob) null)), - Map.entry("setClob(int,Clob)", pstmt -> pstmt.setClob(1, (Clob) null)), Map.entry("setArray(int,Array)", pstmt -> pstmt.setArray(1, null)), Map.entry("setDate(int,Date,Calendar)", pstmt -> pstmt.setDate(1, null, null)), Map.entry("setTime(int,Time,Calendar)", pstmt -> pstmt.setTime(1, null, null)), From 55b223d30d0135f368d68e5e91e62f0b20ab0fd4 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 23 Jan 2025 13:04:26 -0500 Subject: [PATCH 31/38] add isWrapper() implementation to be consistent with metadata PR --- .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 4e22e4e0..ace1afa3 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -280,6 +280,11 @@ public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + @SuppressWarnings("JavaUtilDate") private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Date date, int sqlType) throws SQLException { From 412f38f087ad66853637cba13dbdc1129e948247 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 23 Jan 2025 13:13:13 -0500 Subject: [PATCH 32/38] minor change to PreparedStatementAdapter's javadoc (removing 'by default' at the end) --- .../com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java index af871267..2716c7e9 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/PreparedStatementAdapter.java @@ -39,7 +39,7 @@ import java.util.Calendar; /** - * A {@link java.sql.PreparedStatement} adapter interface that throws exceptions for all its API methods by default. + * A {@link java.sql.PreparedStatement} adapter interface that throws exceptions for all its API methods. * * @see MongoPreparedStatement */ From d1c40062f5ad06eecc33772224ffdd6a8ea90c5b Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 24 Jan 2025 13:37:15 -0500 Subject: [PATCH 33/38] make time related stuff to be implemented in their own ticket --- .../hibernate/jdbc/MongoPreparedStatement.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index ace1afa3..9c5e63b5 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -40,7 +40,6 @@ import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonBoolean; -import org.bson.BsonDateTime; import org.bson.BsonDecimal128; import org.bson.BsonDocument; import org.bson.BsonDouble; @@ -256,21 +255,21 @@ public void setArray(int parameterIndex, @Nullable Array x) throws SQLException public void setDate(int parameterIndex, @Nullable Date x, @Nullable Calendar cal) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - setBsonDateTimeParameter(parameterIndex, x, Types.DATE); + throw new NotYetImplementedException("To implement in scope of https://jira.mongodb.org/browse/HIBERNATE-42"); } @Override public void setTime(int parameterIndex, @Nullable Time x, @Nullable Calendar cal) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - setBsonDateTimeParameter(parameterIndex, x, Types.TIME); + throw new NotYetImplementedException("To implement in scope of https://jira.mongodb.org/browse/HIBERNATE-42"); } @Override public void setTimestamp(int parameterIndex, @Nullable Timestamp x, @Nullable Calendar cal) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - setBsonDateTimeParameter(parameterIndex, x, Types.TIMESTAMP); + throw new NotYetImplementedException("To implement in scope of https://jira.mongodb.org/browse/HIBERNATE-42"); } @Override @@ -285,16 +284,6 @@ public boolean isWrapperFor(Class iface) { return false; } - @SuppressWarnings("JavaUtilDate") - private void setBsonDateTimeParameter(int parameterIndex, java.util.@Nullable Date date, int sqlType) - throws SQLException { - if (date == null) { - setNull(parameterIndex, sqlType); - } else { - setParameter(parameterIndex, new BsonDateTime(date.getTime())); - } - } - private void setParameter(int parameterIndex, BsonValue parameterValue) { var parameterValueSetter = parameterValueSetters.get(parameterIndex - 1); parameterValueSetter.accept(parameterValue); From 1073bf0a4f31982eb632322fa27d50782af08d68 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 24 Jan 2025 14:24:19 -0500 Subject: [PATCH 34/38] improve fail() message in MongoPreparedStatement#parseParameters --- .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 9c5e63b5..8d27f38f 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -317,7 +317,8 @@ private static void parseParameters(BsonValue value, List> p } else if (value.isArray()) { parseParameters(value.asArray(), parameterValueSetters); } else { - fail("Only BSON container type (BsonDocument or BsonArray) is accepted"); + fail("Only BSON container type (BsonDocument or BsonArray) is accepted; provided type: " + + value.getBsonType()); } } From 89429ab13eb55e90047165419ec579384381f3df Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 24 Jan 2025 14:26:49 -0500 Subject: [PATCH 35/38] Update src/test/java/org/mockito/configuration/MockitoConfiguration.java Co-authored-by: Valentin Kovalenko --- .../java/org/mockito/configuration/MockitoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/mockito/configuration/MockitoConfiguration.java b/src/test/java/org/mockito/configuration/MockitoConfiguration.java index e68b9019..cce680a2 100644 --- a/src/test/java/org/mockito/configuration/MockitoConfiguration.java +++ b/src/test/java/org/mockito/configuration/MockitoConfiguration.java @@ -24,7 +24,7 @@ * href="https://github.com/mockito/mockito/issues/971">issue is resolved, this seems the best way to configure * {@link Answers#RETURNS_SMART_NULLS RETURNS_SMART_NULLS} as the default Mock {@link Answer}. */ -public class MockitoConfiguration extends DefaultMockitoConfiguration { +public final class MockitoConfiguration extends DefaultMockitoConfiguration { public Answer getDefaultAnswer() { return Answers.RETURNS_SMART_NULLS; From c71b5d5e410282f77f2d11644b739392e96b6388 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 24 Jan 2025 14:29:03 -0500 Subject: [PATCH 36/38] remove unnecessary RETURNS_SMART_NULLS from MongoPreparedStatementTests and MongoStatementTests --- .../mongodb/hibernate/jdbc/MongoPreparedStatementTests.java | 3 +-- .../java/com/mongodb/hibernate/jdbc/MongoStatementTests.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 2e866d6d..3c794291 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -19,7 +19,6 @@ import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Answers.RETURNS_SMART_NULLS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -85,7 +84,7 @@ class ParameterValueSettingTests { } """; - @Mock(answer = RETURNS_SMART_NULLS) + @Mock private MongoDatabase mongoDatabase; @Captor diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java index b5d6efd9..90ba779b 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoStatementTests.java @@ -20,7 +20,6 @@ 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.Answers.RETURNS_SMART_NULLS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.same; @@ -90,7 +89,7 @@ void testSQLExceptionThrownWhenCalledWithInvalidMql() { @Test @DisplayName("SQLException is thrown when database access error occurs") - void testSQLExceptionThrownWhenDBAccessFailed(@Mock(answer = RETURNS_SMART_NULLS) MongoDatabase mongoDatabase) { + void testSQLExceptionThrownWhenDBAccessFailed(@Mock MongoDatabase mongoDatabase) { // given doReturn(mongoDatabase).when(mongoClient).getDatabase(anyString()); var dbAccessException = new RuntimeException(); From 30004dfb205e7bbdb4429e0a174663130a56e496 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 24 Jan 2025 14:31:41 -0500 Subject: [PATCH 37/38] add isWrapperFor for MongoConnection and MongoStatement --- .../java/com/mongodb/hibernate/jdbc/MongoConnection.java | 5 +++++ .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 5 ----- src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java index 16e64528..7027e307 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java @@ -217,6 +217,11 @@ public void clearWarnings() throws SQLException { checkClosed(); } + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + private void checkClosed() throws SQLException { if (closed) { throw new SQLException("Connection has been closed"); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 8d27f38f..a6d4cecc 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -279,11 +279,6 @@ public void setNull(int parameterIndex, int sqlType, @Nullable String typeName) throw new NotYetImplementedException("To be implemented during Array / Struct tickets"); } - @Override - public boolean isWrapperFor(Class iface) { - return false; - } - private void setParameter(int parameterIndex, BsonValue parameterValue) { var parameterValueSetter = parameterValueSetters.get(parameterIndex - 1); parameterValueSetter.accept(parameterValue); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index e6eedc1f..17f15feb 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -204,6 +204,11 @@ public boolean isClosed() { return closed; } + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + protected void checkClosed() throws SQLException { if (closed) { throw new SQLException(format("%s has been closed", getClass().getSimpleName())); From bdad28d9a8e0f098595d77a950af01c779aa9df3 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 27 Jan 2025 14:33:17 -0500 Subject: [PATCH 38/38] Update src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java Co-authored-by: Valentin Kovalenko --- src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index 17f15feb..f9be5d43 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -209,7 +209,7 @@ public boolean isWrapperFor(Class iface) { return false; } - protected void checkClosed() throws SQLException { + void checkClosed() throws SQLException { if (closed) { throw new SQLException(format("%s has been closed", getClass().getSimpleName())); }