diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java new file mode 100644 index 00000000..871602ab --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.id; + +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.bson.BsonDocument; +import org.bson.BsonObjectId; +import org.bson.types.ObjectId; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@DomainModel(annotatedClasses = {ObjectIdAsIdIntegrationTests.Item.class}) +@ExtendWith(MongoExtension.class) +class ObjectIdAsIdIntegrationTests implements SessionFactoryScopeAware { + @InjectMongoCollection("items") + private static MongoCollection mongoCollection; + + private SessionFactoryScope sessionFactoryScope; + + @Test + void insert() { + var item = new Item(); + item.id = new ObjectId(1, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertThat(mongoCollection.find()) + .containsExactly(new BsonDocument().append(ID_FIELD_NAME, new BsonObjectId(item.id))); + } + + @Test + void getById() { + var item = new Item(); + item.id = new ObjectId(1, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.get(Item.class, item.id)); + assertEquals(item.id, loadedItem.id); + } + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Entity + @Table(name = "items") + static class Item { + @Id + ObjectId id; + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java new file mode 100644 index 00000000..76fde3e6 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.type; + +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.internal.type.ObjectIdJavaType; +import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Objects; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonNull; +import org.bson.BsonObjectId; +import org.bson.types.ObjectId; +import org.hibernate.annotations.JavaType; +import org.hibernate.annotations.JdbcType; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@DomainModel(annotatedClasses = {ObjectIdIntegrationTests.Item.class}) +@ExtendWith(MongoExtension.class) +class ObjectIdIntegrationTests implements SessionFactoryScopeAware { + @InjectMongoCollection("items") + private static MongoCollection mongoCollection; + + private SessionFactoryScope sessionFactoryScope; + + @Test + void insert() { + var item = new Item(); + item.id = 1; + item.v = new ObjectId(1, 0); + item.vExplicitlyAnnotatedNotForThePublic = new ObjectId(2, 3); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertThat(mongoCollection.find()) + .containsExactly(new BsonDocument() + .append(ID_FIELD_NAME, new BsonInt32(1)) + .append("v", new BsonObjectId(item.v)) + .append("vNull", BsonNull.VALUE) + .append( + "vExplicitlyAnnotatedNotForThePublic", + new BsonObjectId(item.vExplicitlyAnnotatedNotForThePublic))); + } + + @Test + void getById() { + var item = new Item(); + item.id = 1; + item.v = new ObjectId(2, 0); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.get(Item.class, item.id)); + assertEquals(item, loadedItem); + } + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Entity + @Table(name = "items") + static class Item { + @Id + int id; + + ObjectId v; + ObjectId vNull; + + @JavaType(ObjectIdJavaType.class) + @JdbcType(ObjectIdJdbcType.class) + ObjectId vExplicitlyAnnotatedNotForThePublic; + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Item item = (Item) o; + return id == item.id + && Objects.equals(v, item.v) + && Objects.equals(vNull, item.vNull) + && Objects.equals(vExplicitlyAnnotatedNotForThePublic, item.vExplicitlyAnnotatedNotForThePublic); + } + + @Override + public int hashCode() { + return Objects.hash(id, v, vNull, vExplicitlyAnnotatedNotForThePublic); + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index c10ceb31..27e7845f 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -17,10 +17,14 @@ package com.mongodb.hibernate.dialect; import com.mongodb.hibernate.internal.translate.MongoTranslatorFactory; +import com.mongodb.hibernate.internal.type.ObjectIdJavaType; +import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; import com.mongodb.hibernate.jdbc.MongoConnectionProvider; +import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslatorFactory; /** @@ -48,4 +52,11 @@ protected DatabaseVersion getMinimumSupportedVersion() { public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { return new MongoTranslatorFactory(); } + + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contribute(typeContributions, serviceRegistry); + typeContributions.contributeJavaType(ObjectIdJavaType.INSTANCE); + typeContributions.contributeJdbcType(ObjectIdJdbcType.INSTANCE); + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java index 27068bc6..73bbb921 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java @@ -20,5 +20,6 @@ public final class MongoConstants { private MongoConstants() {} + public static final String MONGO_DBMS_NAME = "MongoDB"; public static final String ID_FIELD_NAME = "_id"; } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java new file mode 100644 index 00000000..c3486c78 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.type; + +import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; +import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; + +import com.mongodb.hibernate.internal.MongoAssertions; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.sql.SQLType; +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; +import org.hibernate.type.SqlTypes; + +public enum MqlType implements SQLType { + OBJECT_ID(11_000); + + static { + assertTrue(maxHibernateSqlTypeCode() < minMqlTypeCode()); + } + + MqlType(int code) { + this.code = code; + } + + private final int code; + + @Override + public String getName() { + return name(); + } + + @Override + public String getVendor() { + return MONGO_DBMS_NAME; + } + + @Override + public Integer getVendorTypeNumber() { + return code; + } + + private static int minMqlTypeCode() { + return Arrays.stream(MqlType.values()) + .mapToInt(MqlType::getVendorTypeNumber) + .min() + .orElseThrow(MongoAssertions::fail); + } + + private static int maxHibernateSqlTypeCode() { + Predicate publicStaticFinal = field -> { + var modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); + }; + ToIntFunction valueExtractor = field -> { + try { + return field.getInt(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }; + return Arrays.stream(SqlTypes.class.getDeclaredFields()) + .filter(field -> field.getType().equals(int.class)) + .filter(publicStaticFinal) + .mapToInt(valueExtractor) + .max() + .orElseThrow(MongoAssertions::fail); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java new file mode 100644 index 00000000..c19db47f --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java @@ -0,0 +1,67 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.type; + +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import java.io.Serial; +import java.util.concurrent.ThreadLocalRandom; +import org.bson.types.ObjectId; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; + +public final class ObjectIdJavaType extends AbstractClassJavaType { + @Serial + private static final long serialVersionUID = 1L; + + private static final int hashCode = ThreadLocalRandom.current().nextInt(); + + public static final ObjectIdJavaType INSTANCE = new ObjectIdJavaType(); + + private ObjectIdJavaType() { + super(ObjectId.class); + } + + @Override + public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { + return ObjectIdJdbcType.INSTANCE; + } + + @Override + public X unwrap(ObjectId value, Class type, WrapperOptions options) { + throw new FeatureNotSupportedException(); + } + + @Override + public ObjectId wrap(X value, WrapperOptions options) { + if (value instanceof ObjectId wrapped) { + return wrapped; + } + throw new FeatureNotSupportedException(); + } + + @Override + public boolean equals(Object o) { + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return hashCode; + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java new file mode 100644 index 00000000..ac1a13da --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -0,0 +1,127 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.type; + +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import java.io.Serial; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import org.bson.types.ObjectId; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +public final class ObjectIdJdbcType implements JdbcType { + @Serial + private static final long serialVersionUID = 1L; + + public static final ObjectIdJdbcType INSTANCE = new ObjectIdJdbcType(); + private static final MqlType MQL_TYPE = MqlType.OBJECT_ID; + private static final ObjectIdJavaType JAVA_TYPE = ObjectIdJavaType.INSTANCE; + + private ObjectIdJdbcType() {} + + @Override + public int getJdbcTypeCode() { + return MQL_TYPE.getVendorTypeNumber(); + } + + @Override + public String getFriendlyName() { + return MQL_TYPE.getName(); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if (!javaType.equals(JAVA_TYPE)) { + throw new FeatureNotSupportedException(); + } + @SuppressWarnings("unchecked") + var result = (ValueBinder) Binder.INSTANCE; + return result; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if (!javaType.equals(JAVA_TYPE)) { + throw new FeatureNotSupportedException(); + } + @SuppressWarnings("unchecked") + var result = (ValueExtractor) Extractor.INSTANCE; + return result; + } + + /** Thread-safe. */ + private static final class Binder extends BasicBinder { + @Serial + private static final long serialVersionUID = 1L; + + static final Binder INSTANCE = new Binder(); + + private Binder() { + super(JAVA_TYPE, ObjectIdJdbcType.INSTANCE); + } + + @Override + protected void doBind(PreparedStatement st, ObjectId value, int index, WrapperOptions options) + throws SQLException { + st.setObject(index, value, getJdbcType().getJdbcTypeCode()); + } + + @Override + protected void doBind(CallableStatement st, ObjectId value, String name, WrapperOptions options) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + } + + /** Thread-safe. */ + private static final class Extractor extends BasicExtractor { + @Serial + private static final long serialVersionUID = 1L; + + static final Extractor INSTANCE = new Extractor(); + + private Extractor() { + super(JAVA_TYPE, ObjectIdJdbcType.INSTANCE); + } + + @Override + protected ObjectId doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return rs.getObject(paramIndex, getJavaType().getJavaTypeClass()); + } + + @Override + protected ObjectId doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + protected ObjectId doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/package-info.java b/src/main/java/com/mongodb/hibernate/internal/type/package-info.java new file mode 100644 index 00000000..51519547 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** The program elements within this package are not part of the public API and may be removed or changed at any time */ +@NullMarked +package com.mongodb.hibernate.internal.type; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index ccc3c789..200f9303 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -21,6 +21,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; +import com.mongodb.hibernate.internal.type.MqlType; import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; @@ -46,10 +47,12 @@ import org.bson.BsonInt32; import org.bson.BsonInt64; import org.bson.BsonNull; +import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.BsonType; import org.bson.BsonValue; import org.bson.types.Decimal128; +import org.bson.types.ObjectId; final class MongoPreparedStatement extends MongoStatement implements PreparedStatementAdapter { @@ -188,7 +191,17 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + BsonValue value; + if (targetSqlType == MqlType.OBJECT_ID.getVendorTypeNumber()) { + if (x instanceof ObjectId v) { + value = new BsonObjectId(v); + } else { + throw fail(); + } + } else { + throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + } + setParameter(parameterIndex, value); } @Override diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index f0d0ee7e..96ddc440 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -51,6 +51,7 @@ import java.util.function.Function; import org.bson.BsonDocument; import org.bson.BsonValue; +import org.bson.types.ObjectId; import org.jspecify.annotations.Nullable; final class MongoResultSet implements ResultSetAdapter { @@ -204,7 +205,13 @@ public double getDouble(int columnIndex) throws SQLException { public @Nullable T getObject(int columnIndex, Class type) throws SQLException { checkClosed(); checkColumnIndex(columnIndex); - throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + Object value; + if (type.equals(ObjectId.class)) { + value = getValue(columnIndex, bsonValue -> bsonValue.asObjectId().getValue()); + } else { + throw new SQLFeatureNotSupportedException("To be implemented in scope of Array / Struct tickets"); + } + return type.cast(value); } @Override diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index 34c268ea..c62eeda7 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -35,6 +35,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; +import com.mongodb.hibernate.internal.type.MqlType; import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; @@ -45,9 +46,16 @@ import java.sql.Timestamp; import java.sql.Types; import java.util.Calendar; +import java.util.List; import java.util.function.Consumer; +import org.bson.BsonArray; +import org.bson.BsonBoolean; import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonObjectId; +import org.bson.BsonString; import org.bson.Document; +import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -79,16 +87,18 @@ private MongoPreparedStatement createMongoPreparedStatement(String mql) throws S private static final String EXAMPLE_MQL = """ { - insert: "books", + insert: "items", documents: [ { - title: { $undefined: true }, - author: { $undefined: true }, - publishYear: { $undefined: true }, - outOfStock: { $undefined: true }, - tags: [ + string1: { $undefined: true }, + string2: { $undefined: true }, + int32: { $undefined: true }, + boolean: { $undefined: true }, + stringAndObjectId: [ + { $undefined: true }, { $undefined: true } ] + objectId: { $undefined: true } } ] } @@ -110,33 +120,33 @@ void testSuccess() 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); - preparedStatement.setString(5, "classic"); + preparedStatement.setString(1, "s1"); + preparedStatement.setString(2, "s2"); + preparedStatement.setInt(3, 1); + preparedStatement.setBoolean(4, true); + preparedStatement.setString(5, "array element"); + preparedStatement.setObject(6, new ObjectId(1, 2), MqlType.OBJECT_ID.getVendorTypeNumber()); + preparedStatement.setObject(7, new ObjectId(2, 0), MqlType.OBJECT_ID.getVendorTypeNumber()); 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" - ] - } - ] - } - """); + var expectedDoc = new BsonDocument() + .append("insert", new BsonString("items")) + .append( + "documents", + new BsonArray(List.of(new BsonDocument() + .append("string1", new BsonString("s1")) + .append("string2", new BsonString("s2")) + .append("int32", new BsonInt32(1)) + .append("boolean", BsonBoolean.TRUE) + .append( + "stringAndObjectId", + new BsonArray(List.of( + new BsonString("array element"), + new BsonObjectId(new ObjectId(1, 2))))) + .append("objectId", new BsonObjectId(new ObjectId(2, 0)))))); assertEquals(expectedDoc, command); } } @@ -151,7 +161,7 @@ void testParameterIndexUnderflow() throws SQLSyntaxErrorException { @Test void testParameterIndexOverflow() throws SQLSyntaxErrorException { var mongoPreparedStatement = createMongoPreparedStatement(EXAMPLE_MQL); - checkSetterMethods(mongoPreparedStatement, 6, MongoPreparedStatementTests::assertThrowsOutOfRangeException); + checkSetterMethods(mongoPreparedStatement, 8, MongoPreparedStatementTests::assertThrowsOutOfRangeException); } @Nested diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java index a23369cd..d02406ff 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java @@ -41,9 +41,11 @@ import org.bson.BsonInt32; import org.bson.BsonInt64; import org.bson.BsonNull; +import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.types.Decimal128; +import org.bson.types.ObjectId; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -106,7 +108,10 @@ void testGettersForNull() throws SQLException { () -> assertEquals(0, mongoResultSet.getInt(1)), () -> assertEquals(0L, mongoResultSet.getLong(1)), () -> assertEquals(0D, mongoResultSet.getDouble(1)), - () -> assertNull(mongoResultSet.getBigDecimal(1))); + () -> assertNull(mongoResultSet.getBytes(1)), + () -> assertNull(mongoResultSet.getBigDecimal(1)), + () -> assertNull(mongoResultSet.getObject(1, ObjectId.class)), + () -> assertTrue(mongoResultSet.wasNull())); } @Test @@ -118,7 +123,10 @@ void testGettersForBoolean() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -130,7 +138,10 @@ void testGettersForDouble() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertEquals(3.1415, mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -142,7 +153,10 @@ void testGettersForInt() throws SQLException { () -> assertEquals(120, mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -154,7 +168,10 @@ void testGettersForLong() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertEquals(12345678L, mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -166,7 +183,10 @@ void testGettersForString() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -180,7 +200,10 @@ void testGettersForBigDecimal() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), - () -> assertEquals(bigDecimalValue, mongoResultSet.getBigDecimal(1))); + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), + () -> assertEquals(bigDecimalValue, mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } @Test @@ -194,8 +217,27 @@ void testGettersForBinary() throws SQLException { () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), + () -> assertEquals(bytes, mongoResultSet.getBytes(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); + } + + @Test + void testGettersForObject() throws SQLException { + var objectId = new ObjectId(1, 0); + var value = new BsonObjectId(objectId); + createResultSetWith(value); + assertAll( + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getString(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBoolean(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getInt(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getLong(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getDouble(1)), + () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBytes(1)), () -> assertThrowsTypeMismatchException(() -> mongoResultSet.getBigDecimal(1)), - () -> assertEquals(bytes, mongoResultSet.getBytes(1))); + () -> assertEquals(objectId, mongoResultSet.getObject(1, ObjectId.class)), + () -> assertFalse(mongoResultSet.wasNull())); } }