diff --git a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java new file mode 100644 index 00000000..868bb9c8 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java @@ -0,0 +1,773 @@ +/* + * 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; + +import static com.mongodb.hibernate.MongoTestAssertions.assertEq; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hibernate.cfg.AvailableSettings.WRAPPER_ARRAY_HANDLING; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.embeddable.EmbeddableIntegrationTests; +import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests; +import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests.ArraysAndCollections; +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import org.bson.BsonDocument; +import org.bson.types.ObjectId; +import org.hibernate.MappingException; +import org.hibernate.boot.MetadataSources; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@DomainModel( + annotatedClasses = { + ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValues.class, + ArrayAndCollectionIntegrationTests + .ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections.class, + ArrayAndCollectionIntegrationTests.Unsupported.ItemWithBoxedBytesArrayValue.class, + ArrayAndCollectionIntegrationTests.Unsupported.ItemWithBytesCollectionValue.class + }) +@ServiceRegistry(settings = {@Setting(name = WRAPPER_ARRAY_HANDLING, value = "allow")}) +@ExtendWith(MongoExtension.class) +public class ArrayAndCollectionIntegrationTests implements SessionFactoryScopeAware { + @InjectMongoCollection("items") + private static MongoCollection mongoCollection; + + private SessionFactoryScope sessionFactoryScope; + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Test + void testArrayAndCollectionValues() { + var item = new ItemWithArrayAndCollectionValues( + 1, + // TODO-HIBERNATE-48 sprinkle on `null` array/collection elements + new byte[] {2, 3}, + new char[] {'s', 't', 'r'}, + new int[] {5}, + new long[] {Long.MAX_VALUE, 6}, + new double[] {Double.MAX_VALUE}, + new boolean[] {true}, + new Character[] {'s', 't', 'r'}, + new Integer[] {7}, + new Long[] {8L}, + new Double[] {9.1d}, + new Boolean[] {true}, + new String[] {"str"}, + new BigDecimal[] {BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001")}, + new StructAggregateEmbeddableIntegrationTests.Single[] { + new StructAggregateEmbeddableIntegrationTests.Single(1) + }, + List.of('s', 't', 'r'), + List.of(5), + List.of(Long.MAX_VALUE, 6L), + List.of(Double.MAX_VALUE), + List.of(true), + List.of("str"), + List.of(BigDecimal.valueOf(10.1)), + List.of(new ObjectId("000000000000000000000001")), + List.of(new StructAggregateEmbeddableIntegrationTests.Single(1))); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: {$binary: {base64: "AgM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithArrayAndCollectionValues.class, item.id)); + assertEq(item, loadedItem); + var updatedItem = sessionFactoryScope.fromTransaction(session -> { + var result = session.find(ItemWithArrayAndCollectionValues.class, item.id); + result.bytes[0] = (byte) -result.bytes[0]; + result.longs[1] = -result.longs[1]; + result.objectIds[0] = new ObjectId("000000000000000000000002"); + result.longsCollection.remove(6L); + result.longsCollection.add(-6L); + return result; + }); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: {$binary: {base64: "/gM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "-6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000002"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "-6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + } + """); + loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithArrayAndCollectionValues.class, updatedItem.id)); + assertEq(updatedItem, loadedItem); + } + + @Test + void testArrayAndCollectionEmptyValues() { + var item = new ItemWithArrayAndCollectionValues( + 1, + new byte[0], + new char[0], + new int[0], + new long[0], + new double[0], + new boolean[0], + new Character[0], + new Integer[0], + new Long[0], + new Double[0], + new Boolean[0], + new String[0], + new BigDecimal[0], + new ObjectId[0], + new StructAggregateEmbeddableIntegrationTests.Single[0], + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of()); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: {$binary: {base64: "", subType: "0"}}, + chars: "", + ints: [], + longs: [], + doubles: [], + booleans: [], + boxedChars: [], + boxedInts: [], + boxedLongs: [], + boxedDoubles: [], + boxedBooleans: [], + strings: [], + bigDecimals: [], + objectIds: [], + structAggregateEmbeddables: [], + charsCollection: [], + intsCollection: [], + longsCollection: [], + doublesCollection: [], + booleansCollection: [], + stringsCollection: [], + bigDecimalsCollection: [], + objectIdsCollection: [], + structAggregateEmbeddablesCollection: [] + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithArrayAndCollectionValues.class, item.id)); + assertEq(item, loadedItem); + } + + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testArrayAndCollectionNullValues() { + var item = new ItemWithArrayAndCollectionValues( + 1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: null, + chars: null, + ints: null, + longs: null, + doubles: null, + booleans: null, + boxedChars: null, + boxedInts: null, + boxedLongs: null, + boxedDoubles: null, + boxedBooleans: null, + strings: null, + bigDecimals: null, + objectIds: null, + structAggregateEmbeddables: null, + charsCollection: null, + intsCollection: null, + longsCollection: null, + doublesCollection: null, + booleansCollection: null, + stringsCollection: null, + bigDecimalsCollection: null, + objectIdsCollection: null, + structAggregateEmbeddablesCollection: null + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithArrayAndCollectionValues.class, item.id)); + assertEq(item, loadedItem); + } + + @Test + void testArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections() { + var arraysAndCollections = new ArraysAndCollections( + new byte[] {2, 3}, + new char[] {'s', 't', 'r'}, + new int[] {5}, + new long[] {Long.MAX_VALUE, 6}, + new double[] {Double.MAX_VALUE}, + new boolean[] {true}, + new Character[] {'s', 't', 'r'}, + new Integer[] {7}, + new Long[] {8L}, + new Double[] {9.1d}, + new Boolean[] {true}, + new String[] {"str"}, + new BigDecimal[] {BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001")}, + new StructAggregateEmbeddableIntegrationTests.Single[] { + new StructAggregateEmbeddableIntegrationTests.Single(1) + }, + List.of('s', 't', 'r'), + // Hibernate ORM uses `LinkedHashSet`, forcing us to also use it, but messing up the order anyway + new LinkedHashSet<>(List.of(5)), + List.of(Long.MAX_VALUE, 6L), + List.of(Double.MAX_VALUE), + List.of(true), + List.of("str"), + List.of(BigDecimal.valueOf(10.1)), + List.of(new ObjectId("000000000000000000000001")), + List.of(new StructAggregateEmbeddableIntegrationTests.Single(1))); + var item = new ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + 1, new ArraysAndCollections[] {arraysAndCollections}, List.of()); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + structAggregateEmbeddables: [{ + bytes: {$binary: {base64: "AgM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + }], + structAggregateEmbeddablesCollection: [] + } + """); + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.find( + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections.class, item.id)); + assertEq(item, loadedItem); + var updatedItem = sessionFactoryScope.fromTransaction(session -> { + var result = session.find( + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections.class, + item.id); + result.structAggregateEmbeddablesCollection.add(result.structAggregateEmbeddables[0]); + result.structAggregateEmbeddables = new ArraysAndCollections[0]; + return result; + }); + assertCollectionContainsExactly( + """ + { + _id: 1, + structAggregateEmbeddables: [], + structAggregateEmbeddablesCollection: [{ + bytes: {$binary: {base64: "AgM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + }] + } + """); + loadedItem = sessionFactoryScope.fromTransaction(session -> session.find( + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections.class, + updatedItem.id)); + assertEq(updatedItem, loadedItem); + } + + /** + * @see EmbeddableIntegrationTests#testFlattenedValueHavingNullArraysAndCollections() + * @see StructAggregateEmbeddableIntegrationTests#testNestedValueHavingNullArraysAndCollections() + */ + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + public void testArrayAndCollectionValuesOfEmptyStructAggregateEmbeddables() { + var emptyStructAggregateEmbeddable = new ArraysAndCollections( + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null); + var item = new ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + 1, + new ArraysAndCollections[] {emptyStructAggregateEmbeddable}, + List.of(emptyStructAggregateEmbeddable)); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + structAggregateEmbeddables: [{ + bytes: null, + chars: null, + ints: null, + longs: null, + doubles: null, + booleans: null, + boxedChars: null, + boxedInts: null, + boxedLongs: null, + boxedDoubles: null, + boxedBooleans: null, + strings: null, + bigDecimals: null, + objectIds: null, + structAggregateEmbeddables: null, + charsCollection: null, + intsCollection: null, + longsCollection: null, + doublesCollection: null, + booleansCollection: null, + stringsCollection: null, + bigDecimalsCollection: null, + objectIdsCollection: null, + structAggregateEmbeddablesCollection: null + }], + structAggregateEmbeddablesCollection: [{ + bytes: null, + chars: null, + ints: null, + longs: null, + doubles: null, + booleans: null, + boxedChars: null, + boxedInts: null, + boxedLongs: null, + boxedDoubles: null, + boxedBooleans: null, + strings: null, + bigDecimals: null, + objectIds: null, + structAggregateEmbeddables: null, + charsCollection: null, + intsCollection: null, + longsCollection: null, + doublesCollection: null, + booleansCollection: null, + stringsCollection: null, + bigDecimalsCollection: null, + objectIdsCollection: null, + structAggregateEmbeddablesCollection: null + }] + } + """); + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.find( + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections.class, item.id)); + assertEq(item, loadedItem); + } + + private static void assertCollectionContainsExactly(String documentAsJsonObject) { + assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(documentAsJsonObject)); + } + + @Entity + @Table(name = "items") + static class ItemWithArrayAndCollectionValues { + @Id + int id; + + byte[] bytes; + char[] chars; + int[] ints; + long[] longs; + double[] doubles; + boolean[] booleans; + Character[] boxedChars; + Integer[] boxedInts; + Long[] boxedLongs; + Double[] boxedDoubles; + Boolean[] boxedBooleans; + String[] strings; + BigDecimal[] bigDecimals; + ObjectId[] objectIds; + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables; + Collection charsCollection; + Collection intsCollection; + Collection longsCollection; + Collection doublesCollection; + Collection booleansCollection; + Collection stringsCollection; + Collection bigDecimalsCollection; + Collection objectIdsCollection; + Collection structAggregateEmbeddablesCollection; + + ItemWithArrayAndCollectionValues() {} + + ItemWithArrayAndCollectionValues( + int id, + byte[] bytes, + char[] chars, + int[] ints, + long[] longs, + double[] doubles, + boolean[] booleans, + Character[] boxedChars, + Integer[] boxedInts, + Long[] boxedLongs, + Double[] boxedDoubles, + Boolean[] boxedBooleans, + String[] strings, + BigDecimal[] bigDecimals, + ObjectId[] objectIds, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables, + Collection charsCollection, + Collection intsCollection, + Collection longsCollection, + Collection doublesCollection, + Collection booleansCollection, + Collection stringsCollection, + Collection bigDecimalsCollection, + Collection objectIdsCollection, + Collection structAggregateEmbeddablesCollection) { + this.id = id; + this.bytes = bytes; + this.chars = chars; + this.ints = ints; + this.longs = longs; + this.doubles = doubles; + this.booleans = booleans; + this.boxedChars = boxedChars; + this.boxedInts = boxedInts; + this.boxedLongs = boxedLongs; + this.boxedDoubles = boxedDoubles; + this.boxedBooleans = boxedBooleans; + this.strings = strings; + this.bigDecimals = bigDecimals; + this.objectIds = objectIds; + this.structAggregateEmbeddables = structAggregateEmbeddables; + this.charsCollection = charsCollection; + this.intsCollection = intsCollection; + this.longsCollection = longsCollection; + this.doublesCollection = doublesCollection; + this.booleansCollection = booleansCollection; + this.stringsCollection = stringsCollection; + this.bigDecimalsCollection = bigDecimalsCollection; + this.objectIdsCollection = objectIdsCollection; + this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; + } + } + + @Entity + @Table(name = "items") + static class ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections { + @Id + int id; + + ArraysAndCollections[] structAggregateEmbeddables; + Collection structAggregateEmbeddablesCollection; + + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections() {} + + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + int id, + ArraysAndCollections[] structAggregateEmbeddables, + Collection structAggregateEmbeddablesCollection) { + this.id = id; + this.structAggregateEmbeddables = structAggregateEmbeddables; + this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; + } + } + + @Nested + class Unsupported { + /** + * The {@link ClassCastException} caught here manifests a Hibernate ORM bug. The issue goes away if the + * {@link ItemWithBoxedBytesArrayValue#bytes} field is removed. Otherwise, the behavior of this test should have + * been equivalent to {@link #testBytesCollectionValue()}. + */ + @Test + void testBoxedBytesArrayValue() { + var item = new ItemWithBoxedBytesArrayValue(1, new byte[] {1}, new Byte[] {2}); + assertThatThrownBy(() -> sessionFactoryScope.inTransaction(session -> session.persist(item))) + .isInstanceOf(ClassCastException.class); + } + + @Test + void testBytesCollectionValue() { + var item = new ItemWithBytesCollectionValue(1, List.of((byte) 2)); + assertThatThrownBy(() -> sessionFactoryScope.inTransaction(session -> session.persist(item))) + .hasCauseInstanceOf(SQLFeatureNotSupportedException.class); + } + + @Test + void testNestedArrayValue() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithNestedArrayValue.class) + .buildMetadata()) + .isInstanceOf(MappingException.class); + } + + @Test + void testNestedCollectionValue() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithNestedCollectionValue.class) + .buildMetadata()) + .isInstanceOf(MappingException.class); + } + + @Test + void testArrayOfEmbeddablesValue() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithArrayOfEmbeddablesValue.class) + .buildMetadata()) + .isInstanceOf(MappingException.class); + } + + @Test + void testCollectionOfEmbeddablesValue() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithCollectionOfEmbeddablesValue.class) + .buildMetadata()) + .isInstanceOf(MappingException.class); + } + + @Test + void testArrayOfEmbeddablesElementCollectionValue() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithArrayOfEmbeddablesElementCollectionValue.class) + .buildMetadata() + .buildSessionFactory() + .close()) + .isInstanceOf(FeatureNotSupportedException.class); + } + + @Test + void testCollectionOfEmbeddablesElementCollectionValue() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithCollectionOfEmbeddablesElementCollectionValue.class) + .buildMetadata() + .buildSessionFactory() + .close()) + .isInstanceOf(FeatureNotSupportedException.class); + } + + @Entity + @Table(name = "items") + static class ItemWithBoxedBytesArrayValue { + @Id + int id; + + byte[] bytes; + Byte[] boxedBytes; + + ItemWithBoxedBytesArrayValue() {} + + ItemWithBoxedBytesArrayValue(int id, byte[] bytes, Byte[] boxedBytes) { + this.id = id; + this.bytes = bytes; + this.boxedBytes = boxedBytes; + } + } + + @Entity + @Table(name = "items") + static class ItemWithBytesCollectionValue { + @Id + int id; + + Collection bytes; + + ItemWithBytesCollectionValue() {} + + ItemWithBytesCollectionValue(int id, Collection bytes) { + this.id = id; + this.bytes = bytes; + } + } + + /** + * + * Collections cannot be nested, meaning Hibernate does not support mapping {@code List>}, for + * example. + */ + @Entity + @Table(name = "items") + static class ItemWithNestedArrayValue { + @Id + int id; + + int[][] nestedInts; + } + + /** + * + * Collections cannot be nested, meaning Hibernate does not support mapping {@code List>}, for + * example. + */ + @Entity + @Table(name = "items") + static class ItemWithNestedCollectionValue { + @Id + int id; + + Collection> nestedInts; + } + + @Entity + @Table(name = "items") + static class ItemWithArrayOfEmbeddablesValue { + @Id + int id; + + EmbeddableIntegrationTests.Single[] embeddables; + } + + @Entity + @Table(name = "items") + static class ItemWithCollectionOfEmbeddablesValue { + @Id + int id; + + Collection embeddables; + } + + @Entity + @Table(name = "items") + static class ItemWithArrayOfEmbeddablesElementCollectionValue { + @Id + int id; + + @ElementCollection + EmbeddableIntegrationTests.Single[] embeddables; + } + + @Entity + @Table(name = "items") + static class ItemWithCollectionOfEmbeddablesElementCollectionValue { + @Id + int id; + + @ElementCollection + Collection embeddables; + } + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java index 953a1a34..f470d0fa 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java @@ -25,24 +25,28 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.math.BigDecimal; import org.bson.BsonDocument; -import org.hibernate.annotations.DynamicUpdate; +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.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @SessionFactory(exportSchema = false) @DomainModel( - annotatedClasses = {BasicCrudIntegrationTests.Book.class, BasicCrudIntegrationTests.BookDynamicallyUpdated.class + annotatedClasses = { + BasicCrudIntegrationTests.Item.class, + BasicCrudIntegrationTests.ItemDynamicallyUpdated.class, }) @ExtendWith(MongoExtension.class) class BasicCrudIntegrationTests implements SessionFactoryScopeAware { - @InjectMongoCollection("books") + @InjectMongoCollection("items") private static MongoCollection mongoCollection; private SessionFactoryScope sessionFactoryScope; @@ -56,50 +60,79 @@ public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { class InsertTests { @Test void testSimpleEntityInsertion() { - sessionFactoryScope.inTransaction(session -> { - var book = new Book(); - book.id = 1; - book.title = "War and Peace"; - book.author = "Leo Tolstoy"; - book.publishYear = 1867; - session.persist(book); - }); - var expectedDocument = BsonDocument.parse( + sessionFactoryScope.inTransaction(session -> session.persist(new Item( + 1, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")))); + assertCollectionContainsExactly( """ { _id: 1, - title: "War and Peace", - author: "Leo Tolstoy", - publishYear: 1867 - }"""); - assertCollectionContainsExactly(expectedDocument); + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: "c", + boxedInt: 1, + boxedLong: {$numberLong: "9223372036854775807"}, + boxedDouble: {$numberDouble: "1.7976931348623157E308"}, + boxedBoolean: true, + string: "str", + bigDecimal: {$numberDecimal: "10.1"}, + objectId: {$oid: "000000000000000000000001"} + } + """); } @Test - void testEntityWithNullFieldValueInsertion() { - var author = - """ - TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74, - TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 Make sure `book.author` - is set to `null` when we implement `MongoPreparedStatement.setNull` properly."""; - sessionFactoryScope.inTransaction(session -> { - var book = new Book(); - book.id = 1; - book.title = "War and Peace"; - book.author = author; - book.publishYear = 1867; - session.persist(book); - }); - var expectedDocument = BsonDocument.parse( + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testEntityWithNullFieldValuesInsertion() { + sessionFactoryScope.inTransaction(session -> session.persist(new Item( + 1, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + null, + null, + null, + null, + null, + null, + null, + null))); + assertCollectionContainsExactly( """ { _id: 1, - title: "War and Peace", - author: "%s", - publishYear: 1867 - }""" - .formatted(author)); - assertCollectionContainsExactly(expectedDocument); + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: null, + boxedInt: null, + boxedLong: null, + boxedDouble: null, + boxedBoolean: null, + string: null, + bigDecimal: null, + objectId: null + } + """); } } @@ -110,19 +143,26 @@ class DeleteTests { void testSimpleDeletion() { var id = 1; - sessionFactoryScope.inTransaction(session -> { - var book = new Book(); - book.id = id; - book.title = "War and Peace"; - book.author = "Leo Tolstoy"; - book.publishYear = 1867; - session.persist(book); - }); + sessionFactoryScope.inTransaction(session -> session.persist(new Item( + id, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")))); assertThat(mongoCollection.find()).hasSize(1); sessionFactoryScope.inTransaction(session -> { - var book = session.getReference(Book.class, id); - session.remove(book); + var item = session.getReference(Item.class, id); + session.remove(item); }); assertThat(mongoCollection.find()).isEmpty(); @@ -135,44 +175,138 @@ class UpdateTests { @Test void testSimpleUpdate() { sessionFactoryScope.inTransaction(session -> { - var book = new Book(); - book.id = 1; - book.title = "War and Peace"; - book.author = "Leo Tolstoy"; - book.publishYear = 1867; - session.persist(book); + var item = new Item( + 1, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")); + session.persist(item); session.flush(); + item.primitiveBoolean = false; + item.boxedBoolean = false; + }); - book.title = "Resurrection"; - book.publishYear = 1899; + assertCollectionContainsExactly( + """ + { + _id: 1, + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: false, + boxedChar: "c", + boxedInt: 1, + boxedLong: {$numberLong: "9223372036854775807"}, + boxedDouble: {$numberDouble: "1.7976931348623157E308"}, + boxedBoolean: false, + string: "str", + bigDecimal: {$numberDecimal: "10.1"}, + objectId: {$oid: "000000000000000000000001"} + } + """); + } + + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testSimpleUpdateWithNullFieldValues() { + sessionFactoryScope.inTransaction(session -> { + var item = new Item( + 1, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")); + session.persist(item); + session.flush(); + item.boxedChar = null; + item.boxedInt = null; + item.boxedLong = null; + item.boxedDouble = null; + item.boxedBoolean = null; + item.string = null; + item.bigDecimal = null; + item.objectId = null; }); assertCollectionContainsExactly( - BsonDocument.parse( - """ - {"_id": 1, "author": "Leo Tolstoy", "publishYear": 1899, "title": "Resurrection"}\ - """)); + """ + { + _id: 1, + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: null, + boxedInt: null, + boxedLong: null, + boxedDouble: null, + boxedBoolean: null, + string: null, + bigDecimal: null, + objectId: null + } + """); } @Test void testDynamicUpdate() { sessionFactoryScope.inTransaction(session -> { - var book = new BookDynamicallyUpdated(); - book.id = 1; - book.title = "War and Peace"; - book.author = "Leo Tolstoy"; - book.publishYear = 1899; - session.persist(book); + var item = new ItemDynamicallyUpdated(1, true, true); + session.persist(item); session.flush(); + item.primitiveBoolean = false; + item.boxedBoolean = false; + }); + + assertCollectionContainsExactly( + """ + { + _id: 1, + primitiveBoolean: false, + boxedBoolean: false + } + """); + } - book.publishYear = 1867; + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testDynamicUpdateWithNullFieldValues() { + sessionFactoryScope.inTransaction(session -> { + var item = new ItemDynamicallyUpdated(1, false, true); + session.persist(item); + session.flush(); + item.boxedBoolean = null; }); assertCollectionContainsExactly( - BsonDocument.parse( - """ - {"_id": 1, "author": "Leo Tolstoy", "publishYear": 1867, "title": "War and Peace"}\ - """)); + """ + { + _id: 1, + primitiveBoolean: false, + boxedBoolean: null + } + """); } } @@ -180,64 +314,113 @@ void testDynamicUpdate() { class SelectTests { @Test - void testFindByPrimaryKeyWithoutNullValueField() { - var book = new Book(); - book.id = 1; - book.author = "Marcel Proust"; - book.title = "In Search of Lost Time"; - book.publishYear = 1913; - - sessionFactoryScope.inTransaction(session -> session.persist(book)); - var loadedBook = sessionFactoryScope.fromTransaction(session -> session.find(Book.class, 1)); - assertEq(book, loadedBook); + void testFindByPrimaryKey() { + var item = new Item( + 1, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.find(Item.class, item.id)); + assertEq(item, loadedItem); } @Test - void testFindByPrimaryKeyWithNullValueField() { - var book = new Book(); - book.id = 1; - book.title = "Brave New World"; - book.author = - """ - TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74, - TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 Make sure `book.author` - is set to `null` when we implement `MongoPreparedStatement.setNull` properly."""; - book.publishYear = 1932; - - sessionFactoryScope.inTransaction(session -> session.persist(book)); - var loadedBook = sessionFactoryScope.fromTransaction(session -> session.find(Book.class, 1)); - assertEq(book, loadedBook); + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testFindByPrimaryKeyWithNullFieldValues() { + var item = new Item( + 1, 'c', 1, Long.MAX_VALUE, Double.MAX_VALUE, true, null, null, null, null, null, null, null, null); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.find(Item.class, item.id)); + assertEq(item, loadedItem); } } - private static void assertCollectionContainsExactly(BsonDocument expectedDoc) { - assertThat(mongoCollection.find()).containsExactly(expectedDoc); + private static void assertCollectionContainsExactly(String documentAsJsonObject) { + assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(documentAsJsonObject)); } @Entity - @Table(name = "books") - static class Book { + @Table(name = "items") + static class Item { @Id int id; - String title; - - String author; - - int publishYear; + char primitiveChar; + int primitiveInt; + long primitiveLong; + double primitiveDouble; + boolean primitiveBoolean; + Character boxedChar; + Integer boxedInt; + Long boxedLong; + Double boxedDouble; + Boolean boxedBoolean; + String string; + BigDecimal bigDecimal; + ObjectId objectId; + + Item() {} + + Item( + int id, + char primitiveChar, + int primitiveInt, + long primitiveLong, + double primitiveDouble, + boolean primitiveBoolean, + Character boxedChar, + Integer boxedInt, + Long boxedLong, + Double boxedDouble, + Boolean boxedBoolean, + String string, + BigDecimal bigDecimal, + ObjectId objectId) { + this.id = id; + this.primitiveChar = primitiveChar; + this.primitiveInt = primitiveInt; + this.primitiveLong = primitiveLong; + this.primitiveDouble = primitiveDouble; + this.primitiveBoolean = primitiveBoolean; + this.boxedChar = boxedChar; + this.boxedInt = boxedInt; + this.boxedLong = boxedLong; + this.boxedDouble = boxedDouble; + this.boxedBoolean = boxedBoolean; + this.string = string; + this.bigDecimal = bigDecimal; + this.objectId = objectId; + } } @Entity - @Table(name = "books") - @DynamicUpdate - static class BookDynamicallyUpdated { + @Table(name = "items") + static class ItemDynamicallyUpdated { @Id int id; - String title; + boolean primitiveBoolean; + Boolean boxedBoolean; - String author; + ItemDynamicallyUpdated() {} - int publishYear; + ItemDynamicallyUpdated(int id, boolean primitiveBoolean, Boolean boxedBoolean) { + this.id = id; + this.primitiveBoolean = primitiveBoolean; + this.boxedBoolean = boxedBoolean; + } } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/IdentifierIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/IdentifierIntegrationTests.java index e7e33faf..3bf77eec 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/IdentifierIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/IdentifierIntegrationTests.java @@ -87,6 +87,11 @@ class IdentifierIntegrationTests implements SessionFactoryScopeAware { private SessionFactoryScope sessionFactoryScope; + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + @Test void withSpaceAndDotAndMixedCase() { var item = new WithSpaceAndDotAndMixedCase(); @@ -197,11 +202,6 @@ void endingWithRightSquareBracket() { sessionFactoryScope.inTransaction(session -> session.find(EndingWithRightSquareBracket.class, item.id)); } - @Override - public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { - this.sessionFactoryScope = sessionFactoryScope; - } - @Entity @Table(name = WithSpaceAndDotAndMixedCase.COLLECTION_NAME) static class WithSpaceAndDotAndMixedCase { diff --git a/src/integrationTest/java/com/mongodb/hibernate/MongoTestAssertions.java b/src/integrationTest/java/com/mongodb/hibernate/MongoTestAssertions.java index c9372740..87ed6687 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/MongoTestAssertions.java +++ b/src/integrationTest/java/com/mongodb/hibernate/MongoTestAssertions.java @@ -40,11 +40,11 @@ public static void assertUsingRecursiveComparison( @Nullable Object actual, BiConsumer, Object> assertion) { assertion.accept( - assertThat(expected) + assertThat(actual) .usingRecursiveComparison() .usingOverriddenEquals() .withStrictTypeChecking(), - actual); + expected); } /** diff --git a/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java index a7744f2b..58a725ad 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java @@ -20,8 +20,11 @@ import static com.mongodb.hibernate.MongoTestAssertions.assertUsingRecursiveComparison; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNull; import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests; +import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; import jakarta.persistence.AccessType; @@ -31,14 +34,21 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.List; import java.util.Objects; +import java.util.Set; import org.bson.BsonDocument; +import org.bson.types.ObjectId; +import org.hibernate.HibernateException; import org.hibernate.annotations.Parent; import org.hibernate.boot.MetadataSources; 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.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,18 +57,42 @@ @DomainModel( annotatedClasses = { EmbeddableIntegrationTests.ItemWithFlattenedValues.class, - EmbeddableIntegrationTests.ItemWithOmittedEmptyValue.class + EmbeddableIntegrationTests.ItemWithFlattenedValueHavingArraysAndCollections.class, + EmbeddableIntegrationTests.Unsupported.ItemWithFlattenedValueHavingStructAggregateEmbeddable.class }) @ExtendWith(MongoExtension.class) -class EmbeddableIntegrationTests implements SessionFactoryScopeAware { +public class EmbeddableIntegrationTests implements SessionFactoryScopeAware { @InjectMongoCollection("items") private static MongoCollection mongoCollection; private SessionFactoryScope sessionFactoryScope; + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + @Test void testFlattenedValues() { - var item = new ItemWithFlattenedValues(new Single(1), new Single(2), new PairWithParent(3, new Pair(4, 5))); + var item = new ItemWithFlattenedValues( + new Single(1), + new Single(2), + new PairWithParent( + 3, + new Plural( + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")))); item.flattened2.parent = item; sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( @@ -67,8 +101,19 @@ void testFlattenedValues() { _id: 1, flattened1_a: 2, flattened2_a: 3, - flattened2_flattened_a: 4, - flattened2_flattened_b: 5 + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: "c", + boxedInt: 1, + boxedLong: {$numberLong: "9223372036854775807"}, + boxedDouble: {$numberDouble: "1.7976931348623157E308"}, + boxedBoolean: true, + string: "str", + bigDecimal: {$numberDecimal: "10.1"}, + objectId: {$oid: "000000000000000000000001"} } """); var loadedItem = sessionFactoryScope.fromTransaction( @@ -85,8 +130,19 @@ void testFlattenedValues() { _id: 1, flattened1_a: -2, flattened2_a: 3, - flattened2_flattened_a: 4, - flattened2_flattened_b: 5 + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: "c", + boxedInt: 1, + boxedLong: {$numberLong: "9223372036854775807"}, + boxedDouble: {$numberDouble: "1.7976931348623157E308"}, + boxedBoolean: true, + string: "str", + bigDecimal: {$numberDecimal: "10.1"}, + objectId: {$oid: "000000000000000000000001"} } """); loadedItem = sessionFactoryScope.fromTransaction( @@ -95,45 +151,358 @@ void testFlattenedValues() { } @Test - void testFlattenedEmptyValue() { - var item = new ItemWithOmittedEmptyValue(1, new Empty()); + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testFlattenedNullValueOrHavingNulls() { + var item = new ItemWithFlattenedValues( + new Single(1), + null, + new PairWithParent( + 3, + new Plural( + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + null, + null, + null, + null, + null, + null, + null, + null))); + item.flattened2.parent = item; sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( - // Hibernate ORM does not store/read the empty `item.omitted` value. - // See https://hibernate.atlassian.net/browse/HHH-11936 for more details. """ { - _id: 1 + _id: 1, + flattened1_a: null, + flattened2_a: 3, + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: null, + boxedInt: null, + boxedLong: null, + boxedDouble: null, + boxedBoolean: null, + string: null, + bigDecimal: null, + objectId: null } """); - var loadedItem = - sessionFactoryScope.fromTransaction(session -> session.find(ItemWithOmittedEmptyValue.class, item.id)); - assertUsingRecursiveComparison(item, loadedItem, (assertion, actual) -> assertion - .ignoringFields("omitted") - .isEqualTo(actual)); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithFlattenedValues.class, item.flattenedId)); + assertEq(item, loadedItem); var updatedItem = sessionFactoryScope.fromTransaction(session -> { - var result = session.find(ItemWithOmittedEmptyValue.class, item.id); - result.omitted = null; + var result = session.find(ItemWithFlattenedValues.class, item.flattenedId); + result.flattened2.flattened = null; return result; }); assertCollectionContainsExactly( """ { - _id: 1 + _id: 1, + flattened1_a: null, + flattened2_a: 3, + primitiveChar: null, + primitiveInt: null, + primitiveLong: null, + primitiveDouble: null, + primitiveBoolean: null, + boxedChar: null, + boxedInt: null, + boxedLong: null, + boxedDouble: null, + boxedBoolean: null, + string: null, + bigDecimal: null, + objectId: null } """); loadedItem = sessionFactoryScope.fromTransaction( - session -> session.find(ItemWithOmittedEmptyValue.class, updatedItem.id)); + session -> session.find(ItemWithFlattenedValues.class, updatedItem.flattenedId)); assertEq(updatedItem, loadedItem); } - @Override - public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { - this.sessionFactoryScope = sessionFactoryScope; + @Test + void testFlattenedValueHavingArraysAndCollections() { + var item = new ItemWithFlattenedValueHavingArraysAndCollections( + 1, + new ArraysAndCollections( + // TODO-HIBERNATE-48 sprinkle on `null` array/collection elements + new byte[] {2, 3}, + new char[] {'s', 't', 'r'}, + new int[] {5}, + new long[] {Long.MAX_VALUE, 6}, + new double[] {Double.MAX_VALUE}, + new boolean[] {true}, + new Character[] {'s', 't', 'r'}, + new Integer[] {7}, + new Long[] {8L}, + new Double[] {9.1d}, + new Boolean[] {true}, + new String[] {"str"}, + new BigDecimal[] {BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001")}, + new StructAggregateEmbeddableIntegrationTests.Single[] { + new StructAggregateEmbeddableIntegrationTests.Single(1) + }, + List.of('s', 't', 'r'), + Set.of(5), + List.of(Long.MAX_VALUE, 6L), + List.of(Double.MAX_VALUE), + List.of(true), + List.of("str"), + List.of(BigDecimal.valueOf(10.1)), + List.of(new ObjectId("000000000000000000000001")), + List.of(new StructAggregateEmbeddableIntegrationTests.Single(1)))); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: {$binary: {base64: "AgM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, item.id)); + assertEq(item, loadedItem); + var updatedItem = sessionFactoryScope.fromTransaction(session -> { + var result = session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, item.id); + result.flattened.bytes[0] = (byte) -result.flattened.bytes[0]; + result.flattened.longs[1] = -result.flattened.longs[1]; + result.flattened.objectIds[0] = new ObjectId("000000000000000000000002"); + result.flattened.longsCollection.remove(6L); + result.flattened.longsCollection.add(-6L); + return result; + }); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: {$binary: {base64: "/gM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "-6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000002"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "-6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + } + """); + loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, updatedItem.id)); + assertEq(updatedItem, loadedItem); } - private static void assertCollectionContainsExactly(String json) { - assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(json)); + @Test + void testFlattenedValueHavingEmptyArraysAndCollections() { + var item = new ItemWithFlattenedValueHavingArraysAndCollections( + 1, + new ArraysAndCollections( + new byte[0], + new char[0], + new int[0], + new long[0], + new double[0], + new boolean[0], + new Character[0], + new Integer[0], + new Long[0], + new Double[0], + new Boolean[0], + new String[0], + new BigDecimal[0], + new ObjectId[0], + new StructAggregateEmbeddableIntegrationTests.Single[0], + List.of(), + Set.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of())); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: {$binary: {base64: "", subType: "0"}}, + chars: "", + ints: [], + longs: [], + doubles: [], + booleans: [], + boxedChars: [], + boxedInts: [], + boxedLongs: [], + boxedDoubles: [], + boxedBooleans: [], + strings: [], + bigDecimals: [], + objectIds: [], + structAggregateEmbeddables: [], + charsCollection: [], + intsCollection: [], + longsCollection: [], + doublesCollection: [], + booleansCollection: [], + stringsCollection: [], + bigDecimalsCollection: [], + objectIdsCollection: [], + structAggregateEmbeddablesCollection: [] + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, item.id)); + assertEq(item, loadedItem); + } + + /** + * This test also covers the behavior of an empty {@linkplain Embeddable embeddable} value, that is one having + * {@code null} as the value of each of its persistent attributes. + * + * @see StructAggregateEmbeddableIntegrationTests#testNestedValueHavingNullArraysAndCollections() + * @see ArrayAndCollectionIntegrationTests#testArrayAndCollectionValuesOfEmptyStructAggregateEmbeddables() + */ + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + public void testFlattenedValueHavingNullArraysAndCollections() { + var emptyEmbeddable = new ArraysAndCollections( + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null); + var item = new ItemWithFlattenedValueHavingArraysAndCollections(1, emptyEmbeddable); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + bytes: null, + chars: null, + ints: null, + longs: null, + doubles: null, + booleans: null, + boxedChars: null, + boxedInts: null, + boxedLongs: null, + boxedDoubles: null, + boxedBooleans: null, + strings: null, + bigDecimals: null, + objectIds: null, + structAggregateEmbeddables: null, + charsCollection: null, + intsCollection: null, + longsCollection: null, + doublesCollection: null, + booleansCollection: null, + stringsCollection: null, + bigDecimalsCollection: null, + objectIdsCollection: null, + structAggregateEmbeddablesCollection: null + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithFlattenedValueHavingArraysAndCollections.class, item.id)); + // `loadedItem.flattened` is `null` despite `item.flattened` not being `null`. + // There is nothing we can do here, such is the Hibernate ORM behavior. + assertNull(loadedItem.flattened); + assertUsingRecursiveComparison(item, loadedItem, (assertion, expected) -> assertion + .ignoringFields("flattened") + .isEqualTo(expected)); + } + + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testReadNestedValuesMissingFields() { + var insertResult = mongoCollection.insertOne( + BsonDocument.parse( + """ + { + _id: 1, + flattened2_a: 3, + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true + } + """)); + var id = new Single(insertResult.getInsertedId().asInt32().getValue()); + var expectedItem = new ItemWithFlattenedValues( + id, + null, + new PairWithParent( + 3, + new Plural( + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + null, + null, + null, + null, + null, + null, + null, + null))); + expectedItem.flattened2.parent = expectedItem; + var loadedItem = + sessionFactoryScope.fromTransaction(session -> session.find(ItemWithFlattenedValues.class, id)); + assertEq(expectedItem, loadedItem); + } + + private static void assertCollectionContainsExactly(String documentAsJsonObject) { + assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(documentAsJsonObject)); } @Entity @@ -146,8 +515,6 @@ static class ItemWithFlattenedValues { Single flattened1; @AttributeOverride(name = "a", column = @Column(name = "flattened2_a")) - @AttributeOverride(name = "flattened.a", column = @Column(name = "flattened2_flattened_a")) - @AttributeOverride(name = "flattened.b", column = @Column(name = "flattened2_flattened_b")) PairWithParent flattened2; ItemWithFlattenedValues() {} @@ -160,7 +527,7 @@ static class ItemWithFlattenedValues { } @Embeddable - static class Single { + public static class Single { int a; Single() {} @@ -187,13 +554,13 @@ public int hashCode() { @Embeddable static class PairWithParent { int a; - Pair flattened; + Plural flattened; @Parent ItemWithFlattenedValues parent; PairWithParent() {} - PairWithParent(int a, Pair flattened) { + PairWithParent(int a, Plural flattened) { this.a = a; this.flattened = flattened; } @@ -216,42 +583,183 @@ ItemWithFlattenedValues getParent() { } @Embeddable - record Pair(int a, int b) {} + record Plural( + char primitiveChar, + int primitiveInt, + long primitiveLong, + double primitiveDouble, + boolean primitiveBoolean, + Character boxedChar, + Integer boxedInt, + Long boxedLong, + Double boxedDouble, + Boolean boxedBoolean, + String string, + BigDecimal bigDecimal, + ObjectId objectId) {} @Entity @Table(name = "items") - static class ItemWithOmittedEmptyValue { + static class ItemWithFlattenedValueHavingArraysAndCollections { @Id int id; - Empty omitted; + ArraysAndCollections flattened; - ItemWithOmittedEmptyValue() {} + ItemWithFlattenedValueHavingArraysAndCollections() {} - ItemWithOmittedEmptyValue(int id, Empty omitted) { + ItemWithFlattenedValueHavingArraysAndCollections(int id, ArraysAndCollections flattened) { this.id = id; - this.omitted = omitted; + this.flattened = flattened; } } @Embeddable - static class Empty {} + static class ArraysAndCollections { + byte[] bytes; + char[] chars; + int[] ints; + long[] longs; + double[] doubles; + boolean[] booleans; + Character[] boxedChars; + Integer[] boxedInts; + Long[] boxedLongs; + Double[] boxedDoubles; + Boolean[] boxedBooleans; + String[] strings; + BigDecimal[] bigDecimals; + ObjectId[] objectIds; + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables; + List charsCollection; + Set intsCollection; + Collection longsCollection; + Collection doublesCollection; + Collection booleansCollection; + Collection stringsCollection; + Collection bigDecimalsCollection; + Collection objectIdsCollection; + Collection structAggregateEmbeddablesCollection; + + ArraysAndCollections() {} + + ArraysAndCollections( + byte[] bytes, + char[] chars, + int[] ints, + long[] longs, + double[] doubles, + boolean[] booleans, + Character[] boxedChars, + Integer[] boxedInts, + Long[] boxedLongs, + Double[] boxedDoubles, + Boolean[] boxedBooleans, + String[] strings, + BigDecimal[] bigDecimals, + ObjectId[] objectIds, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables, + List charsCollection, + Set intsCollection, + Collection longsCollection, + Collection doublesCollection, + Collection booleansCollection, + Collection stringsCollection, + Collection bigDecimalsCollection, + Collection objectIdsCollection, + Collection structAggregateEmbeddablesCollection) { + this.bytes = bytes; + this.chars = chars; + this.ints = ints; + this.longs = longs; + this.doubles = doubles; + this.booleans = booleans; + this.boxedChars = boxedChars; + this.boxedInts = boxedInts; + this.boxedLongs = boxedLongs; + this.boxedDoubles = boxedDoubles; + this.boxedBooleans = boxedBooleans; + this.strings = strings; + this.bigDecimals = bigDecimals; + this.objectIds = objectIds; + this.structAggregateEmbeddables = structAggregateEmbeddables; + this.charsCollection = charsCollection; + this.intsCollection = intsCollection; + this.longsCollection = longsCollection; + this.doublesCollection = doublesCollection; + this.booleansCollection = booleansCollection; + this.stringsCollection = stringsCollection; + this.bigDecimalsCollection = bigDecimalsCollection; + this.objectIdsCollection = objectIdsCollection; + this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; + } + } @Nested class Unsupported { @Test void testPrimaryKeySpanningMultipleFields() { assertThatThrownBy(() -> new MetadataSources() - .addAnnotatedClass(ItemWithPairAsId.class) + .addAnnotatedClass(ItemWithPluralAsId.class) .buildMetadata()) .hasMessageContaining("does not support primary key spanning multiple columns"); } + @Test + void testStructAggregateEmbeddable() { + var item = new ItemWithFlattenedValueHavingStructAggregateEmbeddable( + 1, + new SingleHavingStructAggregateEmbeddable(new StructAggregateEmbeddableIntegrationTests.Single(2))); + assertThatThrownBy(() -> sessionFactoryScope.inTransaction(session -> session.persist(item))) + .isInstanceOf(HibernateException.class); + } + + @Test + void testNoPersistentAttributes() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithFlattenedValueHavingNoPersistentAttributes.class) + .buildMetadata() + .buildSessionFactory() + .close()) + .isInstanceOf(FeatureNotSupportedException.class) + .hasMessageContaining("must have at least one persistent attribute"); + } + @Entity @Table(name = "items") - static class ItemWithPairAsId { + static class ItemWithPluralAsId { @Id - Pair id; + Plural id; } + + @Entity + @Table(name = "items") + static class ItemWithFlattenedValueHavingStructAggregateEmbeddable { + @Id + int id; + + SingleHavingStructAggregateEmbeddable flattened; + + ItemWithFlattenedValueHavingStructAggregateEmbeddable( + int id, SingleHavingStructAggregateEmbeddable flattened) { + this.id = id; + this.flattened = flattened; + } + } + + @Embeddable + record SingleHavingStructAggregateEmbeddable(StructAggregateEmbeddableIntegrationTests.Single nested) {} + + @Entity + @Table(name = "items") + static class ItemWithFlattenedValueHavingNoPersistentAttributes { + @Id + int id; + + NoPersistentAttributes flattened; + } + + @Embeddable + static class NoPersistentAttributes {} } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java index b4a5eeee..e42bb6d3 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java @@ -20,8 +20,10 @@ import static com.mongodb.hibernate.MongoTestAssertions.assertUsingRecursiveComparison; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertNull; import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; @@ -31,7 +33,13 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Collection; +import java.util.List; +import java.util.Set; import org.bson.BsonDocument; +import org.bson.types.ObjectId; import org.hibernate.annotations.Parent; import org.hibernate.annotations.Struct; import org.hibernate.boot.MetadataSources; @@ -39,6 +47,7 @@ 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.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,25 +56,44 @@ @DomainModel( annotatedClasses = { StructAggregateEmbeddableIntegrationTests.ItemWithNestedValues.class, - StructAggregateEmbeddableIntegrationTests.ItemWithOmittedEmptyValue.class, + StructAggregateEmbeddableIntegrationTests.ItemWithNestedValueHavingArraysAndCollections.class, StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingNonInsertable.class, - StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingAllNonInsertable.class, StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingNonUpdatable.class, - StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithPolymorphicPersistentAttribute.class, - StructAggregateEmbeddableIntegrationTests.Unsupported.Polymorphic.class, - StructAggregateEmbeddableIntegrationTests.Unsupported.Concrete.class + StructAggregateEmbeddableIntegrationTests.Unsupported.ItemWithNestedValueHavingEmbeddable.class }) @ExtendWith(MongoExtension.class) -class StructAggregateEmbeddableIntegrationTests implements SessionFactoryScopeAware { +public class StructAggregateEmbeddableIntegrationTests implements SessionFactoryScopeAware { @InjectMongoCollection("items") private static MongoCollection mongoCollection; private SessionFactoryScope sessionFactoryScope; + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + @Test void testNestedValues() { var item = new ItemWithNestedValues( - new EmbeddableIntegrationTests.Single(1), new Single(2), new PairWithParent(3, new Pair(4, 5))); + new EmbeddableIntegrationTests.Single(1), + new Single(2), + new PairWithParent( + 3, + new Plural( + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + "str", + BigDecimal.valueOf(10.1), + new ObjectId("000000000000000000000001")))); item.nested2.parent = item; sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( @@ -78,8 +106,19 @@ void testNestedValues() { nested2: { a: 3, nested: { - a: 4, - b: 5 + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: "c", + boxedInt: 1, + boxedLong: {$numberLong: "9223372036854775807"}, + boxedDouble: {$numberDouble: "1.7976931348623157E308"}, + boxedBoolean: true, + string: "str", + bigDecimal: {$numberDecimal: "10.1"}, + objectId: {$oid: "000000000000000000000001"} } } } @@ -102,8 +141,19 @@ void testNestedValues() { nested2: { a: 3, nested: { - a: 4, - b: 5 + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: "c", + boxedInt: 1, + boxedLong: {$numberLong: "9223372036854775807"}, + boxedDouble: {$numberDouble: "1.7976931348623157E308"}, + boxedBoolean: true, + string: "str", + bigDecimal: {$numberDecimal: "10.1"}, + objectId: {$oid: "000000000000000000000001"} } } } @@ -114,45 +164,365 @@ void testNestedValues() { } @Test - void testNestedEmptyValue() { - var item = new ItemWithOmittedEmptyValue(1, new Empty()); + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testNestedNullValueOrHavingNulls() { + var item = new ItemWithNestedValues( + new EmbeddableIntegrationTests.Single(1), + null, + new PairWithParent( + 3, + new Plural( + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + null, + null, + null, + null, + null, + null, + null, + null))); + item.nested2.parent = item; sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( - // Hibernate ORM does not store/read the empty `item.omitted` value. - // See https://hibernate.atlassian.net/browse/HHH-11936 for more details. """ { - _id: 1 + _id: 1, + nested1: null, + nested2: { + a: 3, + nested: { + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true, + boxedChar: null, + boxedInt: null, + boxedLong: null, + boxedDouble: null, + boxedBoolean: null, + string: null, + bigDecimal: null, + objectId: null + } + } } """); - var loadedItem = - sessionFactoryScope.fromTransaction(session -> session.find(ItemWithOmittedEmptyValue.class, item.id)); - assertUsingRecursiveComparison(item, loadedItem, (assertion, actual) -> assertion - .ignoringFields("omitted") - .isEqualTo(actual)); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithNestedValues.class, item.flattenedId)); + assertEq(item, loadedItem); var updatedItem = sessionFactoryScope.fromTransaction(session -> { - var result = session.find(ItemWithOmittedEmptyValue.class, item.id); - result.omitted = null; + var result = session.find(ItemWithNestedValues.class, item.flattenedId); + result.nested2.nested = null; return result; }); assertCollectionContainsExactly( """ { - _id: 1 + _id: 1, + nested1: null, + nested2: { + a: 3, + nested: null + } } """); loadedItem = sessionFactoryScope.fromTransaction( - session -> session.find(ItemWithOmittedEmptyValue.class, updatedItem.id)); + session -> session.find(ItemWithNestedValues.class, updatedItem.flattenedId)); assertEq(updatedItem, loadedItem); } - @Override - public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { - this.sessionFactoryScope = sessionFactoryScope; + @Test + void testNestedValueHavingArraysAndCollections() { + var item = new ItemWithNestedValueHavingArraysAndCollections( + 1, + // TODO-HIBERNATE-48 sprinkle on `null` array/collection elements + new ArraysAndCollections( + new byte[] {2, 3}, + new char[] {'s', 't', 'r'}, + new int[] {5}, + new long[] {Long.MAX_VALUE, 6}, + new double[] {Double.MAX_VALUE}, + new boolean[] {true}, + new Character[] {'s', 't', 'r'}, + new Integer[] {7}, + new Long[] {8L}, + new Double[] {9.1d}, + new Boolean[] {true}, + new String[] {"str"}, + new BigDecimal[] {BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001")}, + new Single[] {new Single(1)}, + List.of('s', 't', 'r'), + Set.of(5), + List.of(Long.MAX_VALUE, 6L), + List.of(Double.MAX_VALUE), + List.of(true), + List.of("str"), + List.of(BigDecimal.valueOf(10.1)), + List.of(new ObjectId("000000000000000000000001")), + List.of(new Single(1)))); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + nested: { + bytes: {$binary: {base64: "AgM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + } + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithNestedValueHavingArraysAndCollections.class, item.id)); + assertEq(item, loadedItem); + var updatedItem = sessionFactoryScope.fromTransaction(session -> { + var result = session.find(ItemWithNestedValueHavingArraysAndCollections.class, item.id); + result.nested.bytes[0] = (byte) -result.nested.bytes[0]; + result.nested.longs[1] = -result.nested.longs[1]; + result.nested.objectIds[0] = new ObjectId("000000000000000000000002"); + result.nested.longsCollection.remove(6L); + result.nested.longsCollection.add(-6L); + return result; + }); + assertCollectionContainsExactly( + """ + { + _id: 1, + nested: { + bytes: {$binary: {base64: "/gM=", subType: "0"}}, + chars: "str", + ints: [5], + longs: [{$numberLong: "9223372036854775807"}, {$numberLong: "-6"}], + doubles: [{$numberDouble: "1.7976931348623157E308"}], + booleans: [true], + boxedChars: ["s", "t", "r"], + boxedInts: [7], + boxedLongs: [{$numberLong: "8"}], + boxedDoubles: [{$numberDouble: "9.1"}], + boxedBooleans: [true], + strings: ["str"], + bigDecimals: [{$numberDecimal: "10.1"}], + objectIds: [{$oid: "000000000000000000000002"}], + structAggregateEmbeddables: [{a: 1}], + charsCollection: ["s", "t", "r"], + intsCollection: [5], + longsCollection: [{$numberLong: "9223372036854775807"}, {$numberLong: "-6"}], + doublesCollection: [{$numberDouble: "1.7976931348623157E308"}], + booleansCollection: [true], + stringsCollection: ["str"], + bigDecimalsCollection: [{$numberDecimal: "10.1"}], + objectIdsCollection: [{$oid: "000000000000000000000001"}], + structAggregateEmbeddablesCollection: [{a: 1}] + } + } + """); + loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithNestedValueHavingArraysAndCollections.class, updatedItem.id)); + assertEq(updatedItem, loadedItem); } - private static void assertCollectionContainsExactly(String json) { - assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(json)); + @Test + void testNestedValueHavingEmptyArraysAndCollections() { + var item = new ItemWithNestedValueHavingArraysAndCollections( + 1, + new ArraysAndCollections( + new byte[0], + new char[0], + new int[0], + new long[0], + new double[0], + new boolean[0], + new Character[0], + new Integer[0], + new Long[0], + new Double[0], + new Boolean[0], + new String[0], + new BigDecimal[0], + new ObjectId[0], + new Single[0], + List.of(), + Set.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of())); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + nested: { + bytes: {$binary: {base64: "", subType: "0"}}, + chars: "", + ints: [], + longs: [], + doubles: [], + booleans: [], + boxedChars: [], + boxedInts: [], + boxedLongs: [], + boxedDoubles: [], + boxedBooleans: [], + strings: [], + bigDecimals: [], + objectIds: [], + structAggregateEmbeddables: [], + charsCollection: [], + intsCollection: [], + longsCollection: [], + doublesCollection: [], + booleansCollection: [], + stringsCollection: [], + bigDecimalsCollection: [], + objectIdsCollection: [], + structAggregateEmbeddablesCollection: [] + } + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithNestedValueHavingArraysAndCollections.class, item.id)); + assertEq(item, loadedItem); + } + + /** + * This test also covers the behavior of an empty {@linkplain Struct struct} aggregate {@linkplain Embeddable + * embeddable} value, that is one having {@code null} as the value of each of its persistent attributes. + * + * @see EmbeddableIntegrationTests#testFlattenedValueHavingNullArraysAndCollections() + * @see ArrayAndCollectionIntegrationTests#testArrayAndCollectionValuesOfEmptyStructAggregateEmbeddables() + */ + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + public void testNestedValueHavingNullArraysAndCollections() { + var emptyStructAggregateEmbeddable = new ArraysAndCollections( + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null); + var item = new ItemWithNestedValueHavingArraysAndCollections(1, emptyStructAggregateEmbeddable); + sessionFactoryScope.inTransaction(session -> session.persist(item)); + assertCollectionContainsExactly( + """ + { + _id: 1, + nested: { + bytes: null, + chars: null, + ints: null, + longs: null, + doubles: null, + booleans: null, + boxedChars: null, + boxedInts: null, + boxedLongs: null, + boxedDoubles: null, + boxedBooleans: null, + strings: null, + bigDecimals: null, + objectIds: null, + structAggregateEmbeddables: null, + charsCollection: null, + intsCollection: null, + longsCollection: null, + doublesCollection: null, + booleansCollection: null, + stringsCollection: null, + bigDecimalsCollection: null, + objectIdsCollection: null, + structAggregateEmbeddablesCollection: null + } + } + """); + var loadedItem = sessionFactoryScope.fromTransaction( + session -> session.find(ItemWithNestedValueHavingArraysAndCollections.class, item.id)); + // `loadedItem.nested` is `null` despite `item.nested` not being `null`. + // There is nothing we can do here, such is the Hibernate ORM behavior. + assertNull(loadedItem.nested); + assertUsingRecursiveComparison(item, loadedItem, (assertion, expected) -> assertion + .ignoringFields("nested") + .isEqualTo(expected)); + } + + @Test + @Disabled("TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 enable this test") + void testReadNestedValuesMissingFields() { + var insertResult = mongoCollection.insertOne( + BsonDocument.parse( + """ + { + _id: 1, + nested1: {}, + nested2: { + a: 3, + nested: { + primitiveChar: "c", + primitiveInt: 1, + primitiveLong: {$numberLong: "9223372036854775807"}, + primitiveDouble: {$numberDouble: "1.7976931348623157E308"}, + primitiveBoolean: true + } + } + } + """)); + var id = new EmbeddableIntegrationTests.Single( + insertResult.getInsertedId().asInt32().getValue()); + var expectedItem = new ItemWithNestedValues( + id, + // `loadedItem.nested1` is `null` despite `nested1` not being BSON `Null` in the database. + // There is nothing we can do here, such is the Hibernate ORM behavior. + null, + new PairWithParent( + 3, + new Plural( + 'c', + 1, + Long.MAX_VALUE, + Double.MAX_VALUE, + true, + null, + null, + null, + null, + null, + null, + null, + null))); + expectedItem.nested2.parent = expectedItem; + var loadedItem = sessionFactoryScope.fromTransaction(session -> session.find(ItemWithNestedValues.class, id)); + assertEq(expectedItem, loadedItem); + } + + private static void assertCollectionContainsExactly(String documentAsJsonObject) { + assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(documentAsJsonObject)); } @Entity @@ -176,12 +546,12 @@ static class ItemWithNestedValues { @Embeddable @Struct(name = "Single") - static class Single { - int a; + public static class Single { + public int a; Single() {} - Single(int a) { + public Single(int a) { this.a = a; } } @@ -190,13 +560,13 @@ static class Single { @Struct(name = "PairWithParent") static class PairWithParent { int a; - Pair nested; + Plural nested; @Parent ItemWithNestedValues parent; PairWithParent() {} - PairWithParent(int a, Pair nested) { + PairWithParent(int a, Plural nested) { this.a = a; this.nested = nested; } @@ -219,28 +589,119 @@ ItemWithNestedValues getParent() { } @Embeddable - @Struct(name = "Pair") - record Pair(int a, int b) {} + @Struct(name = "Plural") + record Plural( + char primitiveChar, + int primitiveInt, + long primitiveLong, + double primitiveDouble, + boolean primitiveBoolean, + Character boxedChar, + Integer boxedInt, + Long boxedLong, + Double boxedDouble, + Boolean boxedBoolean, + String string, + BigDecimal bigDecimal, + ObjectId objectId) {} @Entity @Table(name = "items") - static class ItemWithOmittedEmptyValue { + static class ItemWithNestedValueHavingArraysAndCollections { @Id int id; - Empty omitted; + ArraysAndCollections nested; - ItemWithOmittedEmptyValue() {} + ItemWithNestedValueHavingArraysAndCollections() {} - ItemWithOmittedEmptyValue(int id, Empty omitted) { + ItemWithNestedValueHavingArraysAndCollections(int id, ArraysAndCollections nested) { this.id = id; - this.omitted = omitted; + this.nested = nested; } } @Embeddable - @Struct(name = "Empty") - static class Empty {} + @Struct(name = "ArraysAndCollections") + public static class ArraysAndCollections { + byte[] bytes; + char[] chars; + int[] ints; + long[] longs; + double[] doubles; + boolean[] booleans; + Character[] boxedChars; + Integer[] boxedInts; + Long[] boxedLongs; + Double[] boxedDoubles; + Boolean[] boxedBooleans; + String[] strings; + BigDecimal[] bigDecimals; + ObjectId[] objectIds; + Single[] structAggregateEmbeddables; + List charsCollection; + Set intsCollection; + Collection longsCollection; + Collection doublesCollection; + Collection booleansCollection; + Collection stringsCollection; + Collection bigDecimalsCollection; + Collection objectIdsCollection; + Collection structAggregateEmbeddablesCollection; + + ArraysAndCollections() {} + + public ArraysAndCollections( + byte[] bytes, + char[] chars, + int[] ints, + long[] longs, + double[] doubles, + boolean[] booleans, + Character[] boxedChars, + Integer[] boxedInts, + Long[] boxedLongs, + Double[] boxedDoubles, + Boolean[] boxedBooleans, + String[] strings, + BigDecimal[] bigDecimals, + ObjectId[] objectIds, + Single[] structAggregateEmbeddables, + List charsCollection, + Set intsCollection, + Collection longsCollection, + Collection doublesCollection, + Collection booleansCollection, + Collection stringsCollection, + Collection bigDecimalsCollection, + Collection objectIdsCollection, + Collection structAggregateEmbeddablesCollection) { + this.bytes = bytes; + this.chars = chars; + this.ints = ints; + this.longs = longs; + this.doubles = doubles; + this.booleans = booleans; + this.boxedChars = boxedChars; + this.boxedInts = boxedInts; + this.boxedLongs = boxedLongs; + this.boxedDoubles = boxedDoubles; + this.boxedBooleans = boxedBooleans; + this.strings = strings; + this.bigDecimals = bigDecimals; + this.objectIds = objectIds; + this.structAggregateEmbeddables = structAggregateEmbeddables; + this.charsCollection = charsCollection; + this.intsCollection = intsCollection; + this.longsCollection = longsCollection; + this.doublesCollection = doublesCollection; + this.booleansCollection = booleansCollection; + this.stringsCollection = stringsCollection; + this.bigDecimalsCollection = bigDecimalsCollection; + this.objectIdsCollection = objectIdsCollection; + this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; + } + } @Nested class Unsupported { @@ -261,20 +722,13 @@ void testNonInsertable() { @Test void testAllNonInsertable() { - var item = new ItemWithNestedValueHavingAllNonInsertable(1, new PairAllNonInsertable(2, 3)); - sessionFactoryScope.inTransaction(session -> session.persist(item)); - assertCollectionContainsExactly( - // `item.omitted` is considered empty because all its persistent attributes are non-insertable. - // Hibernate ORM does not store/read the empty `item.omitted` value. - // See https://hibernate.atlassian.net/browse/HHH-11936 for more details. - """ - { - _id: 1 - } - """); - assertThatThrownBy(() -> sessionFactoryScope.fromTransaction( - session -> session.find(ItemWithNestedValueHavingAllNonInsertable.class, item.id))) - .isInstanceOf(Exception.class); + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithNestedValueHavingAllNonInsertable.class) + .buildMetadata() + .buildSessionFactory() + .close()) + .isInstanceOf(FeatureNotSupportedException.class) + .hasMessageContaining("must have at least one persistent attribute"); } @Test @@ -288,12 +742,36 @@ void testNonUpdatable() { @Test void testPolymorphic() { - assertThatThrownBy(() -> sessionFactoryScope.inTransaction( - session -> session.persist(new ItemWithPolymorphicPersistentAttribute(1, new Concrete(2))))) + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithPolymorphicPersistentAttribute.class) + .addAnnotatedClass(Polymorphic.class) + .addAnnotatedClass(Concrete.class) + .buildMetadata() + .buildSessionFactory() + .close()) .isInstanceOf(FeatureNotSupportedException.class) .hasMessage("Polymorphic mapping is not supported"); } + @Test + void testEmbeddable() { + var item = new ItemWithNestedValueHavingEmbeddable( + 1, new SingleHavingEmbeddable(new EmbeddableIntegrationTests.Single(2))); + assertThatThrownBy(() -> sessionFactoryScope.inTransaction(session -> session.persist(item))) + .hasRootCauseInstanceOf(SQLFeatureNotSupportedException.class); + } + + @Test + void testNoPersistentAttributes() { + assertThatThrownBy(() -> new MetadataSources() + .addAnnotatedClass(ItemWithNestedValueHavingNoPersistentAttributes.class) + .buildMetadata() + .buildSessionFactory() + .close()) + .isInstanceOf(FeatureNotSupportedException.class) + .hasMessageContaining("must have at least one persistent attribute"); + } + @Entity @Table(name = "items") static class ItemWithSingleAsId { @@ -336,13 +814,6 @@ static class ItemWithNestedValueHavingAllNonInsertable { int id; PairAllNonInsertable omitted; - - ItemWithNestedValueHavingAllNonInsertable() {} - - ItemWithNestedValueHavingAllNonInsertable(int id, PairAllNonInsertable omitted) { - this.id = id; - this.omitted = omitted; - } } @Embeddable @@ -382,5 +853,36 @@ static class Concrete extends Polymorphic { this.a = a; } } + + @Entity + @Table(name = "items") + static class ItemWithNestedValueHavingEmbeddable { + @Id + int id; + + SingleHavingEmbeddable nested; + + ItemWithNestedValueHavingEmbeddable(int id, SingleHavingEmbeddable nested) { + this.id = id; + this.nested = nested; + } + } + + @Embeddable + @Struct(name = "SingleHavingEmbeddable") + record SingleHavingEmbeddable(EmbeddableIntegrationTests.Single flattened) {} + + @Entity + @Table(name = "items") + static class ItemWithNestedValueHavingNoPersistentAttributes { + @Id + int id; + + NoPersistentAttributes nested; + } + + @Embeddable + @Struct(name = "NoPersistentAttributes") + static class NoPersistentAttributes {} } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java index 7f688b50..e0c81215 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/id/ObjectIdAsIdIntegrationTests.java @@ -53,6 +53,11 @@ class ObjectIdAsIdIntegrationTests implements SessionFactoryScopeAware { private SessionFactoryScope sessionFactoryScope; + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + @Test void insert() { var item = new Item(); @@ -70,11 +75,6 @@ void findById() { assertEquals(item.id, loadedItem.id); } - @Override - public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { - this.sessionFactoryScope = sessionFactoryScope; - } - @Nested class Generated { @Test diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java index 0ddd2aac..ff610a2d 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java @@ -42,14 +42,6 @@ abstract class AbstractSelectionQueryIntegrationTests implements SessionFactoryS private TestCommandListener testCommandListener; - SessionFactoryScope getSessionFactoryScope() { - return sessionFactoryScope; - } - - TestCommandListener getTestCommandListener() { - return testCommandListener; - } - @Override public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { this.sessionFactoryScope = sessionFactoryScope; @@ -60,6 +52,14 @@ public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class); } + SessionFactoryScope getSessionFactoryScope() { + return sessionFactoryScope; + } + + TestCommandListener getTestCommandListener() { + return testCommandListener; + } + void assertSelectionQuery( String hql, Class resultType, diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java index 5e454851..19d8c731 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -39,6 +39,11 @@ class SimpleSelectQueryIntegrationTests extends AbstractSelectionQueryIntegratio @Nested class QueryTests { + @BeforeEach + void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingContacts.forEach(session::persist)); + getTestCommandListener().clear(); + } private static final List testingContacts = List.of( new Contact(1, "Bob", 18, Country.USA), @@ -56,12 +61,6 @@ private static List getTestingContacts(int... ids) { .toList(); } - @BeforeEach - void beforeEach() { - getSessionFactoryScope().inTransaction(session -> testingContacts.forEach(session::persist)); - getTestCommandListener().clear(); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void testComparisonByEq(boolean fieldAsLhs) { diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java index 19379ebb..10263698 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -38,6 +38,11 @@ @DomainModel(annotatedClasses = Book.class) class SortingSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { + @BeforeEach + void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); + } private static final List testingBooks = List.of( new Book(1, "War and Peace", 1869, true), @@ -55,12 +60,6 @@ private static List getBooksByIds(int... ids) { .toList(); } - @BeforeEach - void beforeEach() { - getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); - getTestCommandListener().clear(); - } - @ParameterizedTest @ValueSource(strings = {"ASC", "DESC"}) void testOrderBySingleFieldWithoutTies(String sortDirection) { diff --git a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java index 40be8e2a..2fd47d01 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/type/ObjectIdIntegrationTests.java @@ -58,6 +58,11 @@ class ObjectIdIntegrationTests implements SessionFactoryScopeAware { private SessionFactoryScope sessionFactoryScope; + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + @Test void insert() { var item = new Item(); @@ -94,11 +99,6 @@ void findById() { assertEq(item, loadedItem); } - @Override - public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { - this.sessionFactoryScope = sessionFactoryScope; - } - @Nested class Generated { @Test diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java b/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java index dbd12c9c..37183a3b 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java @@ -19,6 +19,7 @@ import static java.lang.String.format; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.type.MongoArrayJdbcType; import com.mongodb.hibernate.internal.type.MongoStructJdbcType; import org.hibernate.dialect.aggregate.AggregateSupportImpl; import org.hibernate.mapping.AggregateColumn; @@ -38,10 +39,11 @@ public String aggregateComponentCustomReadExpression( AggregateColumn aggregateColumn, Column column) { var aggregateColumnType = aggregateColumn.getTypeCode(); - if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { + if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() + || aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { return format( - "unused from %s.aggregateComponentCustomReadExpression", - MongoAggregateSupport.class.getSimpleName()); + "unused from %s.aggregateComponentCustomReadExpression for SQL type code [%d]", + MongoAggregateSupport.class.getSimpleName(), aggregateColumnType); } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType)); } @@ -53,17 +55,19 @@ public String aggregateComponentAssignmentExpression( AggregateColumn aggregateColumn, Column column) { var aggregateColumnType = aggregateColumn.getTypeCode(); - if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { + if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() + || aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { return format( - "unused from %s.aggregateComponentAssignmentExpression", - MongoAggregateSupport.class.getSimpleName()); + "unused from %s.aggregateComponentAssignmentExpression for SQL type code [%d]", + MongoAggregateSupport.class.getSimpleName(), aggregateColumnType); } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType)); } @Override public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTypeCode) { - if (aggregateSqlTypeCode == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { + if (aggregateSqlTypeCode == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() + || aggregateSqlTypeCode == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { return false; } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateSqlTypeCode)); diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index 4aee1d37..87a15f5e 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -20,7 +20,9 @@ import static java.lang.String.format; import com.mongodb.hibernate.internal.translate.MongoTranslatorFactory; +import com.mongodb.hibernate.internal.type.MongoArrayJdbcType; import com.mongodb.hibernate.internal.type.MongoStructJdbcType; +import com.mongodb.hibernate.internal.type.MqlType; import com.mongodb.hibernate.internal.type.ObjectIdJavaType; import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; import com.mongodb.hibernate.jdbc.MongoConnectionProvider; @@ -31,6 +33,7 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.jspecify.annotations.Nullable; /** @@ -91,9 +94,24 @@ public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { @Override public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contribute(typeContributions, serviceRegistry); + contributeObjectIdType(typeContributions); + typeContributions.contributeJdbcTypeConstructor(MongoArrayJdbcType.Constructor.INSTANCE); + typeContributions.contributeJdbcType(MongoStructJdbcType.INSTANCE); + } + + private void contributeObjectIdType(TypeContributions typeContributions) { typeContributions.contributeJavaType(ObjectIdJavaType.INSTANCE); typeContributions.contributeJdbcType(ObjectIdJdbcType.INSTANCE); - typeContributions.contributeJdbcType(MongoStructJdbcType.INSTANCE); + var objectIdTypeCode = MqlType.OBJECT_ID.getVendorTypeNumber(); + typeContributions + .getTypeConfiguration() + .getDdlTypeRegistry() + .addDescriptorIfAbsent(new DdlTypeImpl( + objectIdTypeCode, + format( + "unused from %s.contributeObjectIdType for SQL type code [%d]", + MongoDialect.class.getSimpleName(), objectIdTypeCode), + this)); } @Override @@ -105,4 +123,9 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv public AggregateSupport getAggregateSupport() { return MongoAggregateSupport.INSTANCE; } + + @Override + public boolean supportsStandardArrays() { + return true; + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java b/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java index e5cb1258..e6eec3fb 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoAssertions.java @@ -33,7 +33,7 @@ private MongoAssertions() {} */ public static T assertNotNull(@Nullable T value) throws AssertionError { if (value == null) { - throw new AssertionError(); + throw fail(); } return value; } @@ -69,7 +69,7 @@ public static AssertionError fail() throws AssertionError { */ public static void assertNull(@Nullable Object value) throws AssertionError { if (value != null) { - throw new AssertionError(); + throw fail(); } } @@ -82,7 +82,7 @@ public static void assertNull(@Nullable Object value) throws AssertionError { */ public static boolean assertTrue(boolean value) throws AssertionError { if (!value) { - throw new AssertionError(); + throw fail(); } return true; } @@ -96,8 +96,15 @@ public static boolean assertTrue(boolean value) throws AssertionError { */ public static boolean assertFalse(boolean value) throws AssertionError { if (value) { - throw new AssertionError(); + throw fail(); } return false; } + + public static T assertInstanceOf(@Nullable Object value, Class type) { + if (!type.isInstance(value)) { + throw fail(); + } + return type.cast(value); + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/cfg/MongoConfigurationBuilder.java b/src/main/java/com/mongodb/hibernate/internal/cfg/MongoConfigurationBuilder.java index a0b8f400..2982ac38 100644 --- a/src/main/java/com/mongodb/hibernate/internal/cfg/MongoConfigurationBuilder.java +++ b/src/main/java/com/mongodb/hibernate/internal/cfg/MongoConfigurationBuilder.java @@ -73,15 +73,13 @@ private static final class ConfigPropertiesParser { var jdbcUrl = configurationValues.get(JAKARTA_JDBC_URL); if (jdbcUrl == null) { return null; - } - if (jdbcUrl instanceof String jdbcUrlText) { + } else if (jdbcUrl instanceof String jdbcUrlText) { return parseConnectionString(JAKARTA_JDBC_URL, jdbcUrlText); } else if (jdbcUrl instanceof ConnectionString jdbcUrlConnectionString) { return jdbcUrlConnectionString; - } else { - throw MongoConfigurationBuilder.ConfigPropertiesParser.Exceptions.unsupportedType( - JAKARTA_JDBC_URL, jdbcUrl, String.class, ConnectionString.class); } + throw MongoConfigurationBuilder.ConfigPropertiesParser.Exceptions.unsupportedType( + JAKARTA_JDBC_URL, jdbcUrl, String.class, ConnectionString.class); } private static ConnectionString parseConnectionString(String propertyName, String propertyValue) { diff --git a/src/main/java/com/mongodb/hibernate/internal/extension/MongoAdditionalMappingContributor.java b/src/main/java/com/mongodb/hibernate/internal/extension/MongoAdditionalMappingContributor.java index f0147ecb..0eb77722 100644 --- a/src/main/java/com/mongodb/hibernate/internal/extension/MongoAdditionalMappingContributor.java +++ b/src/main/java/com/mongodb/hibernate/internal/extension/MongoAdditionalMappingContributor.java @@ -57,6 +57,7 @@ public void contribute( InFlightMetadataCollector metadata, ResourceStreamLocator resourceStreamLocator, MetadataBuildingContext buildingContext) { + forbidEmbeddablesWithoutPersistentAttributes(metadata); metadata.getEntityBindings().forEach(persistentClass -> { forbidDynamicInsert(persistentClass); checkColumnNames(persistentClass); @@ -67,7 +68,8 @@ public void contribute( private static void forbidDynamicInsert(PersistentClass persistentClass) { if (persistentClass.useDynamicInsert()) { - throw new FeatureNotSupportedException(format("%s is not supported", DynamicInsert.class.getSimpleName())); + throw new FeatureNotSupportedException( + format("%s: %s is not supported", persistentClass, DynamicInsert.class.getSimpleName())); } } @@ -100,6 +102,15 @@ private static void forbidStructIdentifier(PersistentClass persistentClass) { } } + private static void forbidEmbeddablesWithoutPersistentAttributes(InFlightMetadataCollector metadata) { + metadata.visitRegisteredComponents(component -> { + if (!component.hasAnyInsertableColumns()) { + throw new FeatureNotSupportedException( + format("%s: must have at least one persistent attribute", component)); + } + }); + } + private static void setIdentifierColumnName(PersistentClass persistentClass) { var identifier = persistentClass.getIdentifier(); assertFalse(identifier.hasFormula()); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 22df389a..47d263bc 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -950,6 +950,7 @@ private static boolean isComparingFieldWithValue(ComparisonPredicate comparisonP } private static BsonValue toBsonValue(Object value) { + // TODO-HIBERNATE-74 decide if `value` is nullable try { return ValueConversions.toBsonValue(value); } catch (SQLFeatureNotSupportedException e) { diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MongoArrayJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/MongoArrayJdbcType.java new file mode 100644 index 00000000..1f577645 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/type/MongoArrayJdbcType.java @@ -0,0 +1,80 @@ +/* + * 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 java.io.Serial; +import java.sql.JDBCType; +import java.sql.SQLException; +import org.hibernate.dialect.Dialect; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; +import org.jspecify.annotations.Nullable; + +/** Thread-safe. */ +public final class MongoArrayJdbcType extends ArrayJdbcType { + @Serial + private static final long serialVersionUID = 1L; + + public static final JDBCType JDBC_TYPE = JDBCType.ARRAY; + public static final int HIBERNATE_SQL_TYPE = SqlTypes.STRUCT_ARRAY; + + private MongoArrayJdbcType(JdbcType elementJdbcType) { + super(elementJdbcType); + } + + @Override + public int getJdbcTypeCode() { + var result = super.getJdbcTypeCode(); + assertTrue(result == JDBC_TYPE.getVendorTypeNumber()); + return result; + } + + /** This method is overridden to make it accessible from our code. */ + @Override + protected @Nullable X getArray( + BasicExtractor extractor, java.sql.@Nullable Array array, WrapperOptions options) throws SQLException { + return super.getArray(extractor, array, options); + } + + public static final class Constructor implements JdbcTypeConstructor { + public static final Constructor INSTANCE = new Constructor(); + + private Constructor() {} + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new MongoArrayJdbcType(elementType); + } + + @Override + public int getDefaultSqlTypeCode() { + return JDBC_TYPE.getVendorTypeNumber(); + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java index ae8a573d..f51f0031 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java @@ -17,23 +17,27 @@ package com.mongodb.hibernate.internal.type; import static com.mongodb.hibernate.internal.MongoAssertions.assertFalse; +import static com.mongodb.hibernate.internal.MongoAssertions.assertInstanceOf; import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; import static com.mongodb.hibernate.internal.MongoAssertions.fail; +import static com.mongodb.hibernate.internal.type.ValueConversions.isNull; +import static com.mongodb.hibernate.internal.type.ValueConversions.toArrayDomainValue; import static com.mongodb.hibernate.internal.type.ValueConversions.toBsonValue; import static com.mongodb.hibernate.internal.type.ValueConversions.toDomainValue; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import java.io.Serial; import java.sql.CallableStatement; +import java.sql.Connection; import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.Struct; import org.bson.BsonDocument; import org.bson.BsonValue; -import org.hibernate.annotations.Struct; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.type.descriptor.ValueBinder; @@ -55,7 +59,6 @@ public final class MongoStructJdbcType implements StructJdbcType { public static final JDBCType JDBC_TYPE = JDBCType.STRUCT; private final @Nullable EmbeddableMappingType embeddableMappingType; - private final @Nullable String structTypeName; private MongoStructJdbcType() { @@ -64,10 +67,18 @@ private MongoStructJdbcType() { private MongoStructJdbcType( @Nullable EmbeddableMappingType embeddableMappingType, @Nullable String structTypeName) { + if (embeddableMappingType != null && embeddableMappingType.isPolymorphic()) { + throw new FeatureNotSupportedException("Polymorphic mapping is not supported"); + } this.embeddableMappingType = embeddableMappingType; this.structTypeName = structTypeName; } + @Override + public int getJdbcTypeCode() { + return JDBC_TYPE.getVendorTypeNumber(); + } + @Override public String getStructTypeName() { return assertNotNull(structTypeName); @@ -76,12 +87,12 @@ public String getStructTypeName() { /** * This method may be called multiple times with equal {@code sqlType} and different {@code mappingType}. * - * @param sqlType The {@link Struct#name()}. + * @param sqlType The {@link org.hibernate.annotations.Struct#name()}. */ @Override public AggregateJdbcType resolveAggregateJdbcType( EmbeddableMappingType mappingType, String sqlType, RuntimeModelCreationContext creationContext) { - return new MongoStructJdbcType(mappingType, sqlType); + return new MongoStructJdbcType(assertNotNull(mappingType), assertNotNull(sqlType)); } @Override @@ -89,12 +100,21 @@ public EmbeddableMappingType getEmbeddableMappingType() { return assertNotNull(embeddableMappingType); } + /** + * We replaced this method with {@link #createBsonValue(Object, WrapperOptions)} to make it clear that + * {@link #createJdbcValue(Object, WrapperOptions)} is not called by Hibernate ORM. + */ @Override - public BsonDocument createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException { - var embeddableMappingType = assertNotNull(this.embeddableMappingType); - if (embeddableMappingType.isPolymorphic()) { - throw new FeatureNotSupportedException("Polymorphic mapping is not supported"); + public BsonValue createJdbcValue(@Nullable Object domainValue, WrapperOptions options) { + throw fail(); + } + + private BsonValue createBsonValue(@Nullable Object domainValue, WrapperOptions options) throws SQLException { + if (domainValue == null) { + throw new FeatureNotSupportedException( + "TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 return toBsonValue(domainValue)"); } + var embeddableMappingType = getEmbeddableMappingType(); var result = new BsonDocument(); var jdbcValueCount = embeddableMappingType.getJdbcValueCount(); for (int columnIndex = 0; columnIndex < jdbcValueCount; columnIndex++) { @@ -116,14 +136,14 @@ public BsonDocument createJdbcValue(Object domainValue, WrapperOptions options) } BsonValue bsonValue; var jdbcMapping = jdbcValueSelectable.getJdbcMapping(); - if (jdbcMapping.getJdbcType().getJdbcTypeCode() == JDBC_TYPE.getVendorTypeNumber()) { - if (!(jdbcMapping.getJdbcValueBinder() instanceof Binder structValueBinder)) { - throw fail(); - } - if (!(structValueBinder.getJdbcType() instanceof MongoStructJdbcType structJdbcType)) { - throw fail(); - } - bsonValue = structJdbcType.createJdbcValue(value, options); + var jdbcTypeCode = jdbcMapping.getJdbcType().getJdbcTypeCode(); + if (jdbcTypeCode == getJdbcTypeCode()) { + var structValueBinder = assertInstanceOf(jdbcMapping.getJdbcValueBinder(), Binder.class); + bsonValue = structValueBinder.getJdbcType().createBsonValue(value, options); + } else if (jdbcTypeCode == MongoArrayJdbcType.JDBC_TYPE.getVendorTypeNumber()) { + @SuppressWarnings("unchecked") + ValueBinder valueBinder = jdbcMapping.getJdbcValueBinder(); + bsonValue = toBsonValue(valueBinder.getBindValue(value, options)); } else { bsonValue = toBsonValue(value); } @@ -132,26 +152,52 @@ public BsonDocument createJdbcValue(Object domainValue, WrapperOptions options) return result; } + /** + * @return The {@linkplain Struct#getAttributes() struct attributes}. Though, the way we support {@link Struct} in + * {@link MongoStructJdbcType} does not involve Hibernate ORM ever {@linkplain Connection#createStruct(String, + * Object[]) creating} one. If we extended {@link org.hibernate.dialect.StructJdbcType}, this could have been + * different. + */ @Override - public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) throws SQLException { - if (!(rawJdbcValue instanceof BsonDocument bsonDocument)) { - throw fail(); + public Object @Nullable [] extractJdbcValues(@Nullable Object rawJdbcValue, WrapperOptions options) + throws SQLException { + if (isNull(rawJdbcValue)) { + throw new FeatureNotSupportedException( + "TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 return null"); } - var result = new Object[bsonDocument.size()]; - var elementIdx = 0; - for (var value : bsonDocument.values()) { - assertNotNull(value); - result[elementIdx++] = - value instanceof BsonDocument ? extractJdbcValues(value, options) : toDomainValue(value); + var bsonDocument = assertInstanceOf(rawJdbcValue, BsonDocument.class); + var embeddableMappingType = getEmbeddableMappingType(); + var jdbcValueCount = embeddableMappingType.getJdbcValueCount(); + var result = new Object[jdbcValueCount]; + for (int columnIndex = 0; columnIndex < jdbcValueCount; columnIndex++) { + var jdbcValueSelectable = embeddableMappingType.getJdbcValueSelectable(columnIndex); + assertFalse(jdbcValueSelectable.isFormula()); + var fieldName = jdbcValueSelectable.getSelectableName(); + var value = bsonDocument.get(fieldName); + var jdbcMapping = jdbcValueSelectable.getJdbcMapping(); + var jdbcTypeCode = jdbcMapping.getJdbcType().getJdbcTypeCode(); + Object domainValue; + if (isNull(value)) { + throw new FeatureNotSupportedException( + "TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 domainValue = null"); + } else if (jdbcTypeCode == getJdbcTypeCode()) { + var structValueExtractor = assertInstanceOf(jdbcMapping.getJdbcValueExtractor(), Extractor.class); + domainValue = structValueExtractor.getJdbcType().extractJdbcValues(value, options); + } else if (jdbcTypeCode == MongoArrayJdbcType.JDBC_TYPE.getVendorTypeNumber()) { + var arrayJdbcType = assertInstanceOf(jdbcMapping.getJdbcType(), MongoArrayJdbcType.class); + BasicExtractor jdbcValueExtractor = + assertInstanceOf(jdbcMapping.getJdbcValueExtractor(), BasicExtractor.class); + domainValue = + arrayJdbcType.getArray(jdbcValueExtractor, toArrayDomainValue(assertNotNull(value)), options); + } else { + domainValue = toDomainValue( + assertNotNull(value), jdbcMapping.getMappedJavaType().getJavaTypeClass()); + } + result[columnIndex] = domainValue; } return result; } - @Override - public int getJdbcTypeCode() { - return JDBC_TYPE.getVendorTypeNumber(); - } - @Override public ValueBinder getBinder(JavaType javaType) { return new Binder<>(javaType); @@ -171,12 +217,19 @@ private final class Binder extends BasicBinder { super(javaType, MongoStructJdbcType.this); } + @Override + public MongoStructJdbcType getJdbcType() { + return assertInstanceOf(super.getJdbcType(), MongoStructJdbcType.class); + } + + @Override + public Object getBindValue(@Nullable X value, WrapperOptions options) throws SQLException { + return getJdbcType().createBsonValue(value, options); + } + @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - if (!(getJdbcType() instanceof MongoStructJdbcType structJdbcType)) { - throw fail(); - } - st.setObject(index, structJdbcType.createJdbcValue(value, options), structJdbcType.getJdbcTypeCode()); + st.setObject(index, getBindValue(value, options), getJdbcType().getJdbcTypeCode()); } @Override @@ -195,14 +248,16 @@ private final class Extractor extends BasicExtractor { } @Override - protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { - if (!(getJdbcType() instanceof MongoStructJdbcType structJdbcType)) { - throw fail(); - } + public MongoStructJdbcType getJdbcType() { + return assertInstanceOf(super.getJdbcType(), MongoStructJdbcType.class); + } + + @Override + protected @Nullable X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { var classX = getJavaType().getJavaTypeClass(); assertTrue(classX.equals(Object[].class)); var bsonDocument = rs.getObject(paramIndex, BsonDocument.class); - return classX.cast(structJdbcType.extractJdbcValues(bsonDocument, options)); + return classX.cast(getJdbcType().extractJdbcValues(bsonDocument, options)); } @Override diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java index 12d5b847..8994c2ca 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJavaType.java @@ -16,14 +16,18 @@ package com.mongodb.hibernate.internal.type; +import static com.mongodb.hibernate.internal.type.ValueConversions.toObjectIdDomainValue; + import com.mongodb.hibernate.internal.FeatureNotSupportedException; import java.io.Serial; import java.util.concurrent.ThreadLocalRandom; +import org.bson.BsonValue; 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; +import org.jspecify.annotations.Nullable; /** Thread-safe. */ public final class ObjectIdJavaType extends AbstractClassJavaType { @@ -44,20 +48,26 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { } @Override - public X unwrap(ObjectId value, Class type, WrapperOptions options) { - throw new FeatureNotSupportedException(); + public @Nullable X unwrap(@Nullable ObjectId value, Class type, WrapperOptions options) { + if (type.equals(Object.class)) { + return type.cast(value); + } else { + throw new FeatureNotSupportedException(); + } } @Override - public ObjectId wrap(X value, WrapperOptions options) { - if (!(value instanceof ObjectId wrapped)) { - throw new FeatureNotSupportedException(); + public @Nullable ObjectId wrap(@Nullable X value, WrapperOptions options) { + if (value instanceof ObjectId v) { + return v; + } else if (value instanceof BsonValue v) { + return toObjectIdDomainValue(v); } - return wrapped; + throw new FeatureNotSupportedException(); } @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { return o != null && getClass() == o.getClass(); } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java index da01675f..91be6aa8 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -31,6 +31,7 @@ import org.hibernate.type.descriptor.jdbc.BasicBinder; import org.hibernate.type.descriptor.jdbc.BasicExtractor; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.jspecify.annotations.Nullable; /** Thread-safe. */ public final class ObjectIdJdbcType implements JdbcType { @@ -105,7 +106,8 @@ private Extractor(JavaType javaType) { } @Override - protected ObjectId doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + protected @Nullable ObjectId doExtract(ResultSet rs, int paramIndex, WrapperOptions options) + throws SQLException { return rs.getObject(paramIndex, getJavaType().getJavaTypeClass()); } diff --git a/src/main/java/com/mongodb/hibernate/internal/type/ValueConversions.java b/src/main/java/com/mongodb/hibernate/internal/type/ValueConversions.java index e74c7a29..692103e3 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ValueConversions.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ValueConversions.java @@ -17,23 +17,34 @@ package com.mongodb.hibernate.internal.type; import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; +import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; +import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static java.lang.String.format; +import com.mongodb.hibernate.jdbc.MongoArray; +import java.lang.reflect.Array; import java.math.BigDecimal; +import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.util.ArrayList; +import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonBoolean; 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.BsonObjectId; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; /** * Provides conversion methods between {@link BsonValue}s, which our {@link PreparedStatement}/{@link ResultSet} @@ -43,10 +54,16 @@ public final class ValueConversions { private ValueConversions() {} - public static BsonValue toBsonValue(Object value) throws SQLFeatureNotSupportedException { - assertNotNull(value); - if (value instanceof Boolean v) { + public static BsonValue toBsonValue(@Nullable Object value) throws SQLFeatureNotSupportedException { + if (value == null) { + throw new SQLFeatureNotSupportedException( + "TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 return BsonNull.VALUE"); + } else if (value instanceof BsonDocument v) { + return v; + } else if (value instanceof Boolean v) { return toBsonValue(v.booleanValue()); + } else if (value instanceof Character v) { + return toBsonValue(v.charValue()); } else if (value instanceof Integer v) { return toBsonValue(v.intValue()); } else if (value instanceof Long v) { @@ -59,19 +76,33 @@ public static BsonValue toBsonValue(Object value) throws SQLFeatureNotSupportedE return toBsonValue(v); } else if (value instanceof byte[] v) { return toBsonValue(v); + } else if (value instanceof char[] v) { + return toBsonValue(v); } else if (value instanceof ObjectId v) { return toBsonValue(v); - } else { - throw new SQLFeatureNotSupportedException(format( - "Value [%s] of type [%s] is not supported", - value, value.getClass().getTypeName())); + } else if (value instanceof Object[] v) { + return arrayToBsonValue(v); } + throw new SQLFeatureNotSupportedException(format( + "Value [%s] of type [%s] is not supported", + value, value.getClass().getTypeName())); } public static BsonBoolean toBsonValue(boolean value) { return BsonBoolean.valueOf(value); } + /** + * + * Hibernate ORM maps {@code char}/{@link Character} to {@link JDBCType#CHAR} by default. + * + * @see #toDomainValue(String) + */ + private static BsonString toBsonValue(char value) { + return new BsonString(Character.toString(value)); + } + public static BsonInt32 toBsonValue(int value) { return new BsonInt32(value); } @@ -92,17 +123,59 @@ public static BsonString toBsonValue(String value) { return new BsonString(value); } + /** + * + * Hibernate ORM maps {@code byte[]} to {@link java.sql.JDBCType#VARBINARY} by default. + * + * @see #toByteArrayDomainValue(BsonValue) + * @see #toDomainValue(BsonBinary) + */ public static BsonBinary toBsonValue(byte[] value) { return new BsonBinary(value); } + /** + * + * Hibernate ORM maps {@code char[]} to {@link java.sql.JDBCType#VARCHAR} by default. + * + * @see #toDomainValue(BsonString) + */ + private static BsonString toBsonValue(char[] value) { + return new BsonString(String.valueOf(value)); + } + public static BsonObjectId toBsonValue(ObjectId value) { return new BsonObjectId(value); } - static Object toDomainValue(BsonValue value) throws SQLFeatureNotSupportedException { - assertNotNull(value); - if (value instanceof BsonBoolean v) { + public static BsonArray toBsonValue(java.sql.Array value) throws SQLFeatureNotSupportedException { + Object contents; + try { + contents = value.getArray(); + } catch (SQLException e) { + throw fail(e.toString()); + } + return arrayToBsonValue(contents); + } + + private static BsonArray arrayToBsonValue(Object value) throws SQLFeatureNotSupportedException { + var length = Array.getLength(value); + var elements = new ArrayList(length); + for (var i = 0; i < length; i++) { + elements.add(toBsonValue(Array.get(value, i))); + } + return new BsonArray(elements); + } + + static Object toDomainValue(BsonValue value, Class domainType) throws SQLFeatureNotSupportedException { + if (isNull(value)) { + throw new SQLFeatureNotSupportedException( + "TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 return null"); + } else if (value instanceof BsonDocument v) { + return v; + } else if (value instanceof BsonBoolean v) { return toDomainValue(v); } else if (value instanceof BsonInt32 v) { return toDomainValue(v); @@ -113,16 +186,29 @@ static Object toDomainValue(BsonValue value) throws SQLFeatureNotSupportedExcept } else if (value instanceof BsonDecimal128 v) { return toDomainValue(v); } else if (value instanceof BsonString v) { - return toDomainValue(v); + if (domainType.isArray()) { + return toDomainValue(v); + } else { + return toDomainValue(v, domainType); + } } else if (value instanceof BsonBinary v) { return toDomainValue(v); } else if (value instanceof BsonObjectId v) { return toDomainValue(v); - } else { - throw new SQLFeatureNotSupportedException(format( - "Value [%s] of type [%s] is not supported", - value, value.getClass().getTypeName())); + } else if (value instanceof BsonArray v && domainType.isArray()) { + return toDomainValue(v, assertNotNull(domainType.getComponentType())); } + throw new SQLFeatureNotSupportedException(format( + "Value [%s] of type [%s] is not supported for the domain type [%s]", + value, assertNotNull(value).getClass().getTypeName(), domainType)); + } + + public static boolean isNull(@Nullable Object value) { + return value == null || value instanceof BsonNull; + } + + public static BsonDocument toBsonDocumentDomainValue(BsonValue value) { + return value.asDocument(); } public static boolean toBooleanDomainValue(BsonValue value) { @@ -166,17 +252,33 @@ private static BigDecimal toDomainValue(BsonDecimal128 value) { } public static String toStringDomainValue(BsonValue value) { - return toDomainValue(value.asString()); + return toDomainValue(value.asString(), String.class); } - private static String toDomainValue(BsonString value) { - return value.getValue(); + private static T toDomainValue(BsonString value, Class domainType) { + var v = value.getValue(); + Object result; + if (domainType.equals(Character.class)) { + result = toDomainValue(v); + } else { + result = v; + } + return domainType.cast(result); + } + + /** @see #toBsonValue(char) */ + private static char toDomainValue(String value) { + assertTrue(value.length() == 1); + return value.charAt(0); } + /** @see #toBsonValue(byte[]) */ + @SuppressWarnings("MissingSummary") public static byte[] toByteArrayDomainValue(BsonValue value) { return toDomainValue(value.asBinary()); } + /** @see #toBsonValue(byte[]) */ private static byte[] toDomainValue(BsonBinary value) { return value.asBinary().getData(); } @@ -188,4 +290,23 @@ public static ObjectId toObjectIdDomainValue(BsonValue value) { private static ObjectId toDomainValue(BsonObjectId value) { return value.getValue(); } + + public static MongoArray toArrayDomainValue(BsonValue value) throws SQLFeatureNotSupportedException { + return new MongoArray(toDomainValue(value.asArray(), Object.class)); + } + + private static Object toDomainValue(BsonArray value, Class elementType) throws SQLFeatureNotSupportedException { + var size = value.size(); + var result = Array.newInstance(elementType, size); + for (var i = 0; i < size; i++) { + var element = toDomainValue(value.get(i), elementType); + Array.set(result, i, element); + } + return result; + } + + /** @see #toBsonValue(char[]) */ + private static char[] toDomainValue(BsonString value) { + return toDomainValue(value, String.class).toCharArray(); + } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/ArrayAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/ArrayAdapter.java new file mode 100644 index 00000000..334f6b3a --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/ArrayAdapter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.jdbc; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Map; + +interface ArrayAdapter extends Array { + @Override + default String getBaseTypeName() throws SQLException { + throw new SQLFeatureNotSupportedException("getBaseTypeName not implemented"); + } + + @Override + default int getBaseType() throws SQLException { + throw new SQLFeatureNotSupportedException("getBaseType not implemented"); + } + + @Override + default Object getArray() throws SQLException { + throw new SQLFeatureNotSupportedException("getArray not implemented"); + } + + @Override + default Object getArray(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray not implemented"); + } + + @Override + default Object getArray(long index, int count) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray not implemented"); + } + + @Override + default Object getArray(long index, int count, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray not implemented"); + } + + @Override + default ResultSet getResultSet() throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet not implemented"); + } + + @Override + default ResultSet getResultSet(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet not implemented"); + } + + @Override + default ResultSet getResultSet(long index, int count) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet not implemented"); + } + + @Override + default ResultSet getResultSet(long index, int count, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet not implemented"); + } + + @Override + default void free() throws SQLException { + throw new SQLFeatureNotSupportedException("free not implemented"); + } +} diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoArray.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoArray.java new file mode 100644 index 00000000..51c6faec --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoArray.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.jdbc; + +public final class MongoArray implements ArrayAdapter { + private final Object contents; + + public MongoArray(Object contents) { + this.contents = contents; + } + + @Override + public Object getArray() { + // Hibernate ORM does not call `Connection.getTypeMap`/`setTypeMap`, therefore we are free to ignore it + return contents; + } +} diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java index 1ce4029f..8f7322e0 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoConnection.java @@ -32,7 +32,6 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLWarning; import java.sql.Statement; -import java.sql.Struct; import org.bson.BsonDocument; import org.bson.BsonInt32; import org.jspecify.annotations.Nullable; @@ -152,13 +151,7 @@ public PreparedStatement prepareStatement(String mql, int resultSetType, int res @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { checkClosed(); - throw new SQLFeatureNotSupportedException(); - } - - @Override - public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - checkClosed(); - throw new SQLFeatureNotSupportedException(); + return new MongoArray(elements); } @Override diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 349acd01..f693ae17 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.hibernate.internal.MongoAssertions.assertInstanceOf; import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static com.mongodb.hibernate.internal.type.ValueConversions.toBsonValue; import static java.lang.String.format; @@ -107,7 +108,8 @@ public void setNull(int parameterIndex, int sqlType) throws SQLException { "Unsupported SQL type: " + JDBCType.valueOf(sqlType).getName()); } throw new SQLFeatureNotSupportedException( - "TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74, TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48"); + "TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74, TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48" + + " setParameter(parameterIndex, ValueConversions.toBsonValue((Object) null))"); } @Override @@ -186,15 +188,9 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQ checkParameterIndex(parameterIndex); BsonValue value; if (targetSqlType == ObjectIdJdbcType.MQL_TYPE.getVendorTypeNumber()) { - if (!(x instanceof ObjectId v)) { - throw fail(); - } - value = toBsonValue(v); + value = toBsonValue(assertInstanceOf(x, ObjectId.class)); } else if (targetSqlType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { - if (!(x instanceof BsonDocument v)) { - throw fail(); - } - value = v; + value = assertInstanceOf(x, BsonDocument.class); } else { throw new SQLFeatureNotSupportedException(format( "Parameter value [%s] of SQL type [%d] with index [%d] is not supported", @@ -213,7 +209,7 @@ public void addBatch() throws SQLException { public void setArray(int parameterIndex, Array x) throws SQLException { checkClosed(); checkParameterIndex(parameterIndex); - throw new SQLFeatureNotSupportedException(); + setParameter(parameterIndex, toBsonValue(x)); } @Override diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index 46d7fc9d..12d3f47f 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -37,6 +37,7 @@ import static java.lang.String.format; import com.mongodb.client.MongoCursor; +import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.type.ValueConversions; import java.math.BigDecimal; import java.sql.Array; @@ -49,7 +50,6 @@ import java.util.Calendar; import java.util.List; import java.util.Objects; -import java.util.function.Function; import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.types.ObjectId; @@ -196,7 +196,7 @@ public double getDouble(int columnIndex) throws SQLException { public @Nullable Array getArray(int columnIndex) throws SQLException { checkClosed(); checkColumnIndex(columnIndex); - throw new SQLFeatureNotSupportedException(); + return getValue(columnIndex, ValueConversions::toArrayDomainValue); } @Override @@ -207,7 +207,7 @@ public double getDouble(int columnIndex) throws SQLException { if (type.equals(ObjectId.class)) { value = getValue(columnIndex, ValueConversions::toObjectIdDomainValue); } else if (type.equals(BsonDocument.class)) { - value = getValue(columnIndex, BsonValue::asDocument); + value = getValue(columnIndex, ValueConversions::toBsonDocumentDomainValue); } else { throw new SQLFeatureNotSupportedException( format("Type [%s] for a column with index [%d] is not supported", type, columnIndex)); @@ -243,19 +243,24 @@ private void checkClosed() throws SQLException { } } - private T getValue(int columnIndex, Function toJavaConverter, T defaultValue) + private T getValue(int columnIndex, SqlFunction toJavaConverter, T defaultValue) throws SQLException { return Objects.requireNonNullElse(getValue(columnIndex, toJavaConverter), defaultValue); } - private @Nullable T getValue(int columnIndex, Function toJavaConverter) throws SQLException { + private @Nullable T getValue(int columnIndex, SqlFunction toJavaConverter) throws SQLException { try { var key = getKey(columnIndex); var bsonValue = assertNotNull(currentDocument).get(key); if (bsonValue == null) { - throw new RuntimeException(format("The BSON document field with the name [%s] is missing", key)); + throw new FeatureNotSupportedException( + """ + TODO-HIBERNATE-48 https://jira.mongodb.org/browse/HIBERNATE-48 + simply remove this `null` check, as we support missing fields + and they are equivalent to BSON `Null`, which is handled below + """); } - T value = bsonValue.isNull() ? null : toJavaConverter.apply(bsonValue); + T value = ValueConversions.isNull(bsonValue) ? null : toJavaConverter.apply(assertNotNull(bsonValue)); lastReadColumnValueWasNull = value == null; return value; } catch (RuntimeException e) { @@ -272,4 +277,8 @@ private void checkColumnIndex(int columnIndex) throws SQLException { } private static final class MongoResultSetMetadata implements ResultSetMetaDataAdapter {} + + private interface SqlFunction { + R apply(T t) throws SQLException; + } } diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java index 162bbc29..16f0c51b 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoConnectionTests.java @@ -141,8 +141,6 @@ private void checkMethodsWithOpenPrecondition() { () -> mongoConnection.prepareStatement(exampleQueryMql, TYPE_FORWARD_ONLY, CONCUR_READ_ONLY)), () -> assertThrowsClosedException( () -> mongoConnection.createArrayOf("myArrayType", new String[] {"value1", "value2"})), - () -> assertThrowsClosedException( - () -> mongoConnection.createStruct("myStructType", new Object[] {1, "Toronto"})), () -> assertThrowsClosedException(mongoConnection::getMetaData), () -> assertThrowsClosedException(mongoConnection::getCatalog), () -> assertThrowsClosedException(mongoConnection::getSchema), diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java index d02406ff..1b8f35db 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoResultSetTests.java @@ -101,7 +101,7 @@ private void createResultSetWith(BsonValue value) throws SQLException { @Test void testGettersForNull() throws SQLException { - createResultSetWith(new BsonNull()); + createResultSetWith(BsonNull.VALUE); assertAll( () -> assertNull(mongoResultSet.getString(1)), () -> assertFalse(mongoResultSet.getBoolean(1)),