diff --git a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java index c656704c..7a32bb31 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java @@ -16,7 +16,12 @@ package com.mongodb.hibernate; +import static com.mongodb.client.model.Aggregates.project; +import static com.mongodb.client.model.Projections.include; +import static com.mongodb.hibernate.ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValues.CONSTRUCTOR_MAPPING_FOR_ITEM; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.COLLECTION_NAME; import static com.mongodb.hibernate.MongoTestAssertions.assertEq; +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -29,17 +34,21 @@ import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.ColumnResult; +import jakarta.persistence.ConstructorResult; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.SqlResultSetMapping; import jakarta.persistence.Table; import java.math.BigDecimal; import java.sql.SQLFeatureNotSupportedException; import java.time.Instant; import java.util.Collection; -import java.util.LinkedHashSet; +import java.util.HashSet; import java.util.List; import org.bson.BsonDocument; +import org.bson.conversions.Bson; import org.bson.types.ObjectId; import org.hibernate.MappingException; import org.hibernate.boot.MetadataSources; @@ -65,8 +74,6 @@ @ServiceRegistry(settings = {@Setting(name = WRAPPER_ARRAY_HANDLING, value = "allow")}) @ExtendWith(MongoExtension.class) public class ArrayAndCollectionIntegrationTests implements SessionFactoryScopeAware { - private static final String COLLECTION_NAME = "items"; - @InjectMongoCollection(COLLECTION_NAME) private static MongoCollection mongoCollection; @@ -100,7 +107,7 @@ void testArrayAndCollectionValues() { new StructAggregateEmbeddableIntegrationTests.Single(1), null }, asList('s', 't', null, 'r'), - asList(null, 5), + new HashSet<>(asList(null, 5)), asList(Long.MAX_VALUE, null, 6L), asList(null, Double.MAX_VALUE), asList(null, true), @@ -262,8 +269,33 @@ void testArrayAndCollectionEmptyValues() { @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, null, null); + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + (Collection) null, + null, + null, + null, + null, + null, + null, + null, + null, + null); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( """ @@ -324,8 +356,7 @@ void testArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndColl 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)), + new HashSet<>(List.of(5)), List.of(Long.MAX_VALUE, 6L), List.of(Double.MAX_VALUE), List.of(true), @@ -509,11 +540,88 @@ private static void assertCollectionContainsExactly(String documentAsJsonObject) assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(documentAsJsonObject)); } + /** @see BasicCrudIntegrationTests.Item */ @Entity @Table(name = COLLECTION_NAME) - static class ItemWithArrayAndCollectionValues { + @SqlResultSetMapping( + name = CONSTRUCTOR_MAPPING_FOR_ITEM, + classes = + @ConstructorResult( + targetClass = ItemWithArrayAndCollectionValues.class, + columns = { + @ColumnResult(name = ID_FIELD_NAME, type = int.class), + @ColumnResult(name = "bytes", type = byte[].class), + @ColumnResult(name = "chars", type = char[].class), + @ColumnResult(name = "ints", type = int[].class), + @ColumnResult(name = "longs", type = long[].class), + @ColumnResult(name = "doubles", type = double[].class), + @ColumnResult(name = "booleans", type = boolean[].class), + @ColumnResult(name = "boxedChars", type = Character[].class), + @ColumnResult(name = "boxedInts", type = Integer[].class), + @ColumnResult(name = "boxedLongs", type = Long[].class), + @ColumnResult(name = "boxedDoubles", type = Double[].class), + @ColumnResult(name = "boxedBooleans", type = Boolean[].class), + @ColumnResult(name = "strings", type = String[].class), + @ColumnResult(name = "bigDecimals", type = BigDecimal[].class), + @ColumnResult(name = "objectIds", type = ObjectId[].class), + @ColumnResult(name = "instants", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddables", + type = StructAggregateEmbeddableIntegrationTests.Single[].class), + @ColumnResult(name = "charsCollection", type = Character[].class), + @ColumnResult(name = "intsCollection", type = Integer[].class), + @ColumnResult(name = "longsCollection", type = Long[].class), + @ColumnResult(name = "doublesCollection", type = Double[].class), + @ColumnResult(name = "booleansCollection", type = Boolean[].class), + @ColumnResult(name = "stringsCollection", type = String[].class), + @ColumnResult(name = "bigDecimalsCollection", type = BigDecimal[].class), + @ColumnResult(name = "objectIdsCollection", type = ObjectId[].class), + @ColumnResult(name = "instantsCollection", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddablesCollection", + type = StructAggregateEmbeddableIntegrationTests.Single[].class) + })) + @SqlResultSetMapping( + name = ItemWithArrayAndCollectionValues.MAPPING_FOR_ITEM, + columns = { + @ColumnResult(name = ID_FIELD_NAME), + @ColumnResult(name = "bytes", type = byte[].class), + @ColumnResult(name = "chars", type = char[].class), + @ColumnResult(name = "ints", type = int[].class), + @ColumnResult(name = "longs", type = long[].class), + @ColumnResult(name = "doubles", type = double[].class), + @ColumnResult(name = "booleans", type = boolean[].class), + @ColumnResult(name = "boxedChars", type = Character[].class), + @ColumnResult(name = "boxedInts", type = Integer[].class), + @ColumnResult(name = "boxedLongs", type = Long[].class), + @ColumnResult(name = "boxedDoubles", type = Double[].class), + @ColumnResult(name = "boxedBooleans", type = Boolean[].class), + @ColumnResult(name = "strings", type = String[].class), + @ColumnResult(name = "bigDecimals", type = BigDecimal[].class), + @ColumnResult(name = "objectIds", type = ObjectId[].class), + @ColumnResult(name = "instants", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddables", + type = StructAggregateEmbeddableIntegrationTests.Single[].class), + @ColumnResult(name = "charsCollection", type = Character[].class), + @ColumnResult(name = "intsCollection", type = Integer[].class), + @ColumnResult(name = "longsCollection", type = Long[].class), + @ColumnResult(name = "doublesCollection", type = Double[].class), + @ColumnResult(name = "booleansCollection", type = Boolean[].class), + @ColumnResult(name = "stringsCollection", type = String[].class), + @ColumnResult(name = "bigDecimalsCollection", type = BigDecimal[].class), + @ColumnResult(name = "objectIdsCollection", type = ObjectId[].class), + @ColumnResult(name = "instantsCollection", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddablesCollection", + type = StructAggregateEmbeddableIntegrationTests.Single[].class) + }) + public static class ItemWithArrayAndCollectionValues { + public static final String CONSTRUCTOR_MAPPING_FOR_ITEM = "ConstructorItemWithArrayAndCollectionValues"; + public static final String MAPPING_FOR_ITEM = "ItemWithArrayAndCollectionValues"; + @Id - int id; + public int id; byte[] bytes; char[] chars; @@ -544,7 +652,7 @@ static class ItemWithArrayAndCollectionValues { ItemWithArrayAndCollectionValues() {} - ItemWithArrayAndCollectionValues( + public ItemWithArrayAndCollectionValues( int id, byte[] bytes, char[] chars, @@ -600,20 +708,139 @@ static class ItemWithArrayAndCollectionValues { this.instantsCollection = instantsCollection; this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; } + + 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, + Instant[] instants, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables, + Character[] charsCollection, + Integer[] intsCollection, + Long[] longsCollection, + Double[] doublesCollection, + Boolean[] booleansCollection, + String[] stringsCollection, + BigDecimal[] bigDecimalsCollection, + ObjectId[] objectIdsCollection, + Instant[] instantsCollection, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddablesCollection) { + this( + id, + bytes, + chars, + ints, + longs, + doubles, + booleans, + boxedChars, + boxedInts, + boxedLongs, + boxedDoubles, + boxedBooleans, + strings, + bigDecimals, + objectIds, + instants, + structAggregateEmbeddables, + charsCollection == null ? null : asList(charsCollection), + intsCollection == null ? null : new HashSet<>(asList(intsCollection)), + longsCollection == null ? null : asList(longsCollection), + doublesCollection == null ? null : asList(doublesCollection), + booleansCollection == null ? null : asList(booleansCollection), + stringsCollection == null ? null : asList(stringsCollection), + bigDecimalsCollection == null ? null : asList(bigDecimalsCollection), + objectIdsCollection == null ? null : asList(objectIdsCollection), + instantsCollection == null ? null : asList(instantsCollection), + structAggregateEmbeddablesCollection == null ? null : asList(structAggregateEmbeddablesCollection)); + } + + public static Bson projectAll() { + return project(include( + ID_FIELD_NAME, + "bytes", + "chars", + "ints", + "longs", + "doubles", + "booleans", + "boxedChars", + "boxedInts", + "boxedLongs", + "boxedDoubles", + "boxedBooleans", + "strings", + "bigDecimals", + "objectIds", + "instants", + "structAggregateEmbeddables", + "charsCollection", + "intsCollection", + "longsCollection", + "doublesCollection", + "booleansCollection", + "stringsCollection", + "bigDecimalsCollection", + "objectIdsCollection", + "instantsCollection", + "structAggregateEmbeddablesCollection")); + } } @Entity @Table(name = COLLECTION_NAME) - static class ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections { + @SqlResultSetMapping( + name = + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .CONSTRUCTOR_MAPPING_FOR_ITEM, + classes = + @ConstructorResult( + targetClass = + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .class, + columns = { + @ColumnResult(name = ID_FIELD_NAME, type = int.class), + @ColumnResult(name = "structAggregateEmbeddables", type = ArraysAndCollections[].class), + @ColumnResult( + name = "structAggregateEmbeddablesCollection", + type = ArraysAndCollections[].class) + })) + @SqlResultSetMapping( + name = + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .MAPPING_FOR_ITEM, + columns = { + @ColumnResult(name = ID_FIELD_NAME), + @ColumnResult(name = "structAggregateEmbeddables", type = ArraysAndCollections[].class), + @ColumnResult(name = "structAggregateEmbeddablesCollection", type = ArraysAndCollections[].class) + }) + public static class ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections { + public static final String CONSTRUCTOR_MAPPING_FOR_ITEM = + "ConstructorItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections"; + public static final String MAPPING_FOR_ITEM = + "ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections"; + @Id - int id; + public int id; ArraysAndCollections[] structAggregateEmbeddables; Collection structAggregateEmbeddablesCollection; ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections() {} - ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + public ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( int id, ArraysAndCollections[] structAggregateEmbeddables, Collection structAggregateEmbeddablesCollection) { @@ -621,6 +848,18 @@ static class ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingA this.structAggregateEmbeddables = structAggregateEmbeddables; this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; } + + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + int id, + ArraysAndCollections[] structAggregateEmbeddables, + ArraysAndCollections[] structAggregateEmbeddablesCollection) { + this(id, structAggregateEmbeddables, asList(structAggregateEmbeddablesCollection)); + } + + public static Bson projectAll() { + return project( + include(ID_FIELD_NAME, "structAggregateEmbeddables", "structAggregateEmbeddablesCollection")); + } } @Nested diff --git a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java index 762f1948..c31ef43c 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java @@ -16,18 +16,29 @@ package com.mongodb.hibernate; +import static com.mongodb.client.model.Aggregates.project; +import static com.mongodb.client.model.Projections.include; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.CONSTRUCTOR_MAPPING_FOR_ITEM; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.MAPPING_FOR_ITEM; import static com.mongodb.hibernate.MongoTestAssertions.assertEq; +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static org.assertj.core.api.Assertions.assertThat; import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.embeddable.EmbeddableIntegrationTests; +import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.ColumnResult; +import jakarta.persistence.ConstructorResult; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.SqlResultSetMapping; import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.Instant; import org.bson.BsonDocument; +import org.bson.conversions.Bson; import org.bson.types.ObjectId; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; @@ -44,10 +55,9 @@ BasicCrudIntegrationTests.ItemDynamicallyUpdated.class, }) @ExtendWith(MongoExtension.class) -class BasicCrudIntegrationTests implements SessionFactoryScopeAware { - private static final String COLLECTION_NAME = "items"; +public class BasicCrudIntegrationTests implements SessionFactoryScopeAware { - @InjectMongoCollection(COLLECTION_NAME) + @InjectMongoCollection(Item.COLLECTION_NAME) private static MongoCollection mongoCollection; private SessionFactoryScope sessionFactoryScope; @@ -374,30 +384,83 @@ private static void assertCollectionContainsExactly(String documentAsJsonObject) assertThat(mongoCollection.find()).containsExactly(BsonDocument.parse(documentAsJsonObject)); } + /** + * This class should have persistent attributes of all the basic + * types we support. When adding more persistent attributes to this class, we should do similar changes to + * {@link EmbeddableIntegrationTests.Plural}/{@link StructAggregateEmbeddableIntegrationTests.Plural}, + * {@link EmbeddableIntegrationTests.ArraysAndCollections}/{@link StructAggregateEmbeddableIntegrationTests.ArraysAndCollections}, + * {@link ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValues}. + */ @Entity - @Table(name = COLLECTION_NAME) - static class Item { - @Id - int id; + @Table(name = Item.COLLECTION_NAME) + @SqlResultSetMapping( + name = CONSTRUCTOR_MAPPING_FOR_ITEM, + classes = + @ConstructorResult( + targetClass = Item.class, + columns = { + @ColumnResult(name = ID_FIELD_NAME, type = int.class), + @ColumnResult(name = "primitiveChar", type = char.class), + @ColumnResult(name = "primitiveInt", type = int.class), + @ColumnResult(name = "primitiveLong", type = long.class), + @ColumnResult(name = "primitiveDouble", type = double.class), + @ColumnResult(name = "primitiveBoolean", type = boolean.class), + @ColumnResult(name = "boxedChar", type = Character.class), + @ColumnResult(name = "boxedInt", type = Integer.class), + @ColumnResult(name = "boxedLong", type = Long.class), + @ColumnResult(name = "boxedDouble", type = Double.class), + @ColumnResult(name = "boxedBoolean", type = Boolean.class), + @ColumnResult(name = "string", type = String.class), + @ColumnResult(name = "bigDecimal", type = BigDecimal.class), + @ColumnResult(name = "objectId", type = ObjectId.class), + @ColumnResult(name = "instant", type = Instant.class), + })) + @SqlResultSetMapping( + name = MAPPING_FOR_ITEM, + columns = { + @ColumnResult(name = ID_FIELD_NAME), + @ColumnResult(name = "primitiveChar", type = char.class), + @ColumnResult(name = "primitiveInt"), + @ColumnResult(name = "primitiveLong"), + @ColumnResult(name = "primitiveDouble"), + @ColumnResult(name = "primitiveBoolean"), + @ColumnResult(name = "boxedChar", type = Character.class), + @ColumnResult(name = "boxedInt"), + @ColumnResult(name = "boxedLong"), + @ColumnResult(name = "boxedDouble"), + @ColumnResult(name = "boxedBoolean"), + @ColumnResult(name = "string"), + @ColumnResult(name = "bigDecimal"), + @ColumnResult(name = "objectId"), + @ColumnResult(name = "instant") + }) + public static class Item { + public static final String COLLECTION_NAME = "items"; + public static final String CONSTRUCTOR_MAPPING_FOR_ITEM = "ConstructorItem"; + public static final String MAPPING_FOR_ITEM = "Item"; - 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; - Instant instant; + @Id + public int id; + + public char primitiveChar; + public int primitiveInt; + public long primitiveLong; + public double primitiveDouble; + public boolean primitiveBoolean; + public Character boxedChar; + public Integer boxedInt; + public Long boxedLong; + public Double boxedDouble; + public Boolean boxedBoolean; + public String string; + public BigDecimal bigDecimal; + public ObjectId objectId; + public Instant instant; Item() {} - Item( + public Item( int id, char primitiveChar, int primitiveInt, @@ -429,10 +492,29 @@ static class Item { this.objectId = objectId; this.instant = instant; } + + public static Bson projectAll() { + return project(include( + ID_FIELD_NAME, + "primitiveChar", + "primitiveInt", + "primitiveLong", + "primitiveDouble", + "primitiveBoolean", + "boxedChar", + "boxedInt", + "boxedLong", + "boxedDouble", + "boxedBoolean", + "string", + "bigDecimal", + "objectId", + "instant")); + } } @Entity - @Table(name = COLLECTION_NAME) + @Table(name = Item.COLLECTION_NAME) static class ItemDynamicallyUpdated { @Id int id; diff --git a/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java index b4f3d94e..a364e306 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java @@ -16,6 +16,7 @@ package com.mongodb.hibernate.embeddable; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.COLLECTION_NAME; import static com.mongodb.hibernate.MongoTestAssertions.assertEq; import static com.mongodb.hibernate.MongoTestAssertions.assertUsingRecursiveComparison; import static java.util.Arrays.asList; @@ -25,6 +26,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests; +import com.mongodb.hibernate.BasicCrudIntegrationTests; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; @@ -64,8 +66,6 @@ }) @ExtendWith(MongoExtension.class) public class EmbeddableIntegrationTests implements SessionFactoryScopeAware { - private static final String COLLECTION_NAME = "items"; - @InjectMongoCollection(COLLECTION_NAME) private static MongoCollection mongoCollection; @@ -432,8 +432,32 @@ void testFlattenedValueHavingEmptyArraysAndCollections() { @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, null, null); + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + (List) null, + null, + null, + null, + null, + null, + null, + null, + null, + null); var item = new ItemWithFlattenedValueHavingArraysAndCollections(1, emptyEmbeddable); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( @@ -601,8 +625,9 @@ ItemWithFlattenedValues getParent() { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable - record Plural( + public record Plural( char primitiveChar, int primitiveInt, long primitiveLong, @@ -634,8 +659,9 @@ static class ItemWithFlattenedValueHavingArraysAndCollections { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable - static class ArraysAndCollections { + public static class ArraysAndCollections { byte[] bytes; char[] chars; int[] ints; @@ -665,7 +691,7 @@ static class ArraysAndCollections { ArraysAndCollections() {} - ArraysAndCollections( + public ArraysAndCollections( byte[] bytes, char[] chars, int[] ints, @@ -719,6 +745,62 @@ static class ArraysAndCollections { this.instantsCollection = instantsCollection; this.structAggregateEmbeddablesCollection = structAggregateEmbeddablesCollection; } + + 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, + Instant[] instants, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables, + Character[] charsCollection, + Integer[] intsCollection, + Long[] longsCollection, + Double[] doublesCollection, + Boolean[] booleansCollection, + String[] stringsCollection, + BigDecimal[] bigDecimalsCollection, + ObjectId[] objectIdsCollection, + Instant[] instantsCollection, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddablesCollection) { + this( + bytes, + chars, + ints, + longs, + doubles, + booleans, + boxedChars, + boxedInts, + boxedLongs, + boxedDoubles, + boxedBooleans, + strings, + bigDecimals, + objectIds, + instants, + structAggregateEmbeddables, + charsCollection == null ? null : asList(charsCollection), + intsCollection == null ? null : new HashSet<>(asList(intsCollection)), + longsCollection == null ? null : asList(longsCollection), + doublesCollection == null ? null : asList(doublesCollection), + booleansCollection == null ? null : asList(booleansCollection), + stringsCollection == null ? null : asList(stringsCollection), + bigDecimalsCollection == null ? null : asList(bigDecimalsCollection), + objectIdsCollection == null ? null : asList(objectIdsCollection), + instantsCollection == null ? null : asList(instantsCollection), + structAggregateEmbeddablesCollection == null ? null : asList(structAggregateEmbeddablesCollection)); + } } @Nested diff --git a/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java index b3fc25a1..ee63b7bd 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java @@ -16,6 +16,7 @@ package com.mongodb.hibernate.embeddable; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.COLLECTION_NAME; import static com.mongodb.hibernate.MongoTestAssertions.assertEq; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; @@ -23,6 +24,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests; +import com.mongodb.hibernate.BasicCrudIntegrationTests; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.junit.MongoExtension; @@ -63,8 +65,6 @@ }) @ExtendWith(MongoExtension.class) public class StructAggregateEmbeddableIntegrationTests implements SessionFactoryScopeAware { - private static final String COLLECTION_NAME = "items"; - @InjectMongoCollection(COLLECTION_NAME) private static MongoCollection mongoCollection; @@ -597,9 +597,10 @@ ItemWithNestedValues getParent() { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable @Struct(name = "Plural") - record Plural( + public record Plural( char primitiveChar, int primitiveInt, long primitiveLong, @@ -631,6 +632,7 @@ static class ItemWithNestedValueHavingArraysAndCollections { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable @Struct(name = "ArraysAndCollections") public static class ArraysAndCollections { diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java index 82e9a57d..7f433656 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java @@ -19,7 +19,7 @@ import static com.mongodb.hibernate.MongoTestAssertions.assertIterableEq; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.hibernate.cfg.JdbcSettings.DIALECT; +import static org.hibernate.cfg.AvailableSettings.DIALECT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.spy; diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java index ea6bac63..4c6b2c34 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java @@ -16,11 +16,18 @@ package com.mongodb.hibernate.query.mutation; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.COLLECTION_NAME; + import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; import com.mongodb.hibernate.junit.InjectMongoCollection; import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; import com.mongodb.hibernate.query.Book; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.util.List; import java.util.Set; import org.bson.BsonDocument; @@ -29,7 +36,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -@DomainModel(annotatedClasses = Book.class) +@DomainModel(annotatedClasses = {Book.class, UpdatingIntegrationTests.Unsupported.ItemWithNestedValue.class}) class UpdatingIntegrationTests extends AbstractQueryIntegrationTests { @InjectMongoCollection(Book.COLLECTION_NAME) @@ -349,5 +356,29 @@ void testPathExpressionAssignment() { FeatureNotSupportedException.class, "Path expression as update assignment value for field path [publishYear] is not supported"); } + + @Test + void testStructAggregateEmbeddablePathExpressionAssignment() { + assertMutationQueryFailure( + "update ItemWithNestedValue set nested.a = 0", + null, + FeatureNotSupportedException.class, + MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); + } + + @Entity(name = "ItemWithNestedValue") + @Table(name = COLLECTION_NAME) + static class ItemWithNestedValue { + @Id + int id; + + StructAggregateEmbeddableIntegrationTests.Single nested; + + ItemWithNestedValue() {} + + ItemWithNestedValue(StructAggregateEmbeddableIntegrationTests.Single nested) { + this.nested = nested; + } + } } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java index 4d6c2ee5..b365d663 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -19,8 +19,8 @@ import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.cfg.JdbcSettings.DIALECT; -import static org.hibernate.cfg.QuerySettings.QUERY_PLAN_CACHE_ENABLED; +import static org.hibernate.cfg.AvailableSettings.DIALECT; +import static org.hibernate.cfg.AvailableSettings.QUERY_PLAN_CACHE_ENABLED; import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; import com.mongodb.hibernate.dialect.MongoDialect; @@ -460,7 +460,7 @@ void testBothFirstResultAndMaxResultsConflicting() { } @Nested - class FeatureNotSupportedTests { + class Unsupported { @ParameterizedTest @EnumSource(value = FetchClauseType.class, mode = EXCLUDE, names = "ROWS_ONLY") diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java new file mode 100644 index 00000000..1bdaf42f --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -0,0 +1,898 @@ +/* + * 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.query.select; + +import static com.mongodb.client.model.Aggregates.match; +import static com.mongodb.client.model.Aggregates.project; +import static com.mongodb.client.model.Filters.eq; +import static com.mongodb.client.model.Projections.fields; +import static com.mongodb.client.model.Projections.include; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.COLLECTION_NAME; +import static com.mongodb.hibernate.MongoTestAssertions.assertEq; +import static com.mongodb.hibernate.internal.MongoConstants.EXTENDED_JSON_WRITER_SETTINGS; +import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hibernate.cfg.AvailableSettings.WRAPPER_ARRAY_HANDLING; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.mongodb.client.model.Projections; +import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests; +import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValues; +import com.mongodb.hibernate.ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections; +import com.mongodb.hibernate.BasicCrudIntegrationTests; +import com.mongodb.hibernate.BasicCrudIntegrationTests.Item; +import com.mongodb.hibernate.embeddable.EmbeddableIntegrationTests; +import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; +import com.mongodb.hibernate.junit.MongoExtension; +import jakarta.persistence.ColumnResult; +import jakarta.persistence.ConstructorResult; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.SqlResultSetMapping; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import java.math.BigDecimal; +import java.sql.SQLException; +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.conversions.Bson; +import org.bson.types.ObjectId; +import org.hibernate.query.QueryProducer; +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.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@DomainModel( + annotatedClasses = { + Item.class, + NativeQueryIntegrationTests.ItemWithFlattenedValue.class, + NativeQueryIntegrationTests.ItemWithFlattenedValueHavingArraysAndCollections.class, + NativeQueryIntegrationTests.ItemWithNestedValue.class, + NativeQueryIntegrationTests.ItemWithNestedValueHavingArraysAndCollections.class, + ItemWithArrayAndCollectionValues.class, + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections.class + }) +@ServiceRegistry(settings = {@Setting(name = WRAPPER_ARRAY_HANDLING, value = "allow")}) +@ExtendWith(MongoExtension.class) +class NativeQueryIntegrationTests implements SessionFactoryScopeAware { + private SessionFactoryScope sessionFactoryScope; + private Item item; + private ItemWithFlattenedValue itemWithFlattenedValue; + private ItemWithFlattenedValueHavingArraysAndCollections itemWithFlattenedValueHavingArraysAndCollections; + private ItemWithNestedValue itemWithNestedValue; + private ItemWithNestedValueHavingArraysAndCollections itemWithNestedValueHavingArraysAndCollections; + private ItemWithArrayAndCollectionValues itemWithArrayAndCollectionValues; + private ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections; + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @BeforeEach + void beforeEach() { + 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"), + Instant.parse("2024-01-01T10:00:00Z")); + itemWithFlattenedValue = new ItemWithFlattenedValue( + 2, + new EmbeddableIntegrationTests.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"), + Instant.parse("2024-01-01T10:00:00Z"))); + itemWithFlattenedValueHavingArraysAndCollections = new ItemWithFlattenedValueHavingArraysAndCollections( + 3, + new EmbeddableIntegrationTests.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', null, 't', 'r'}, + new Integer[] {null, 7}, + new Long[] {8L, null}, + new Double[] {9.1d, null}, + new Boolean[] {true, null}, + new String[] {null, "str"}, + new BigDecimal[] {null, BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001"), null}, + new Instant[] {Instant.parse("2007-12-03T10:15:30Z"), null}, + new StructAggregateEmbeddableIntegrationTests.Single[] { + new StructAggregateEmbeddableIntegrationTests.Single(1), null + }, + asList('s', 't', null, 'r'), + new HashSet<>(asList(null, 5)), + asList(Long.MAX_VALUE, null, 6L), + asList(null, Double.MAX_VALUE), + asList(null, true), + asList("str", null), + asList(BigDecimal.valueOf(10.1), null), + asList(null, new ObjectId("000000000000000000000001")), + asList(Instant.parse("2007-12-03T10:15:30Z"), null), + asList(new StructAggregateEmbeddableIntegrationTests.Single(1), null))); + itemWithNestedValue = new ItemWithNestedValue( + 4, + new StructAggregateEmbeddableIntegrationTests.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"), + Instant.parse("2007-12-03T10:15:30Z"))); + var arraysAndCollections = new StructAggregateEmbeddableIntegrationTests.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', null, 't', 'r'}, + new Integer[] {null, 7}, + new Long[] {8L, null}, + new Double[] {9.1d, null}, + new Boolean[] {true, null}, + new String[] {null, "str"}, + new BigDecimal[] {null, BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001"), null}, + new Instant[] {Instant.parse("2007-12-03T10:15:30Z"), null}, + new StructAggregateEmbeddableIntegrationTests.Single[] { + new StructAggregateEmbeddableIntegrationTests.Single(1), null + }, + asList('s', 't', null, 'r'), + new HashSet<>(asList(null, 5)), + asList(Long.MAX_VALUE, null, 6L), + asList(null, Double.MAX_VALUE), + asList(null, true), + asList("str", null), + asList(BigDecimal.valueOf(10.1), null), + asList(null, new ObjectId("000000000000000000000001")), + asList(Instant.parse("2007-12-03T10:15:30Z"), null), + asList(new StructAggregateEmbeddableIntegrationTests.Single(1), null)); + itemWithNestedValueHavingArraysAndCollections = + new ItemWithNestedValueHavingArraysAndCollections(5, arraysAndCollections); + itemWithArrayAndCollectionValues = new ItemWithArrayAndCollectionValues( + 6, + 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', null, 't', 'r'}, + new Integer[] {null, 7}, + new Long[] {8L, null}, + new Double[] {9.1d, null}, + new Boolean[] {true, null}, + new String[] {null, "str"}, + new BigDecimal[] {null, BigDecimal.valueOf(10.1)}, + new ObjectId[] {new ObjectId("000000000000000000000001"), null}, + new Instant[] {Instant.parse("2007-12-03T10:15:30Z"), null}, + new StructAggregateEmbeddableIntegrationTests.Single[] { + new StructAggregateEmbeddableIntegrationTests.Single(1), null + }, + asList('s', 't', null, 'r'), + new HashSet<>(asList(null, 5)), + asList(Long.MAX_VALUE, null, 6L), + asList(null, Double.MAX_VALUE), + asList(null, true), + asList("str", null), + asList(BigDecimal.valueOf(10.1), null), + asList(null, new ObjectId("000000000000000000000001")), + asList(Instant.parse("2007-12-03T10:15:30Z"), null), + asList(new StructAggregateEmbeddableIntegrationTests.Single(1), null)); + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections = + new ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + 7, + new StructAggregateEmbeddableIntegrationTests.ArraysAndCollections[] {arraysAndCollections}, + List.of(arraysAndCollections)); + sessionFactoryScope.inTransaction(session -> { + session.persist(item); + session.persist(itemWithFlattenedValue); + session.persist(itemWithFlattenedValueHavingArraysAndCollections); + session.persist(itemWithNestedValue); + session.persist(itemWithNestedValueHavingArraysAndCollections); + session.persist(itemWithArrayAndCollectionValues); + session.persist(itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections); + }); + } + + /** + * See Entity + * queries, {@link QueryProducer#createNativeQuery(String, Class)}. + * + * @see Unsupported#testEntityWithAggregateEmbeddableValue() + */ + @Test + void testEntity() { + sessionFactoryScope.inSession(session -> { + assertAll( + () -> { + var mql = mql(COLLECTION_NAME, List.of(match(eq(item.id)), Item.projectAll())); + assertEq( + item, session.createNativeQuery(mql, Item.class).getSingleResult()); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of(match(eq(itemWithFlattenedValue.id)), ItemWithFlattenedValue.projectAll())); + assertEq( + itemWithFlattenedValue, + session.createNativeQuery(mql, ItemWithFlattenedValue.class) + .getSingleResult()); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithFlattenedValueHavingArraysAndCollections.id)), + ItemWithFlattenedValueHavingArraysAndCollections.projectAll())); + assertEq( + itemWithFlattenedValueHavingArraysAndCollections, + session.createNativeQuery(mql, ItemWithFlattenedValueHavingArraysAndCollections.class) + .getSingleResult()); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithArrayAndCollectionValues.id)), + ItemWithArrayAndCollectionValues.projectAll())); + assertEq( + itemWithArrayAndCollectionValues, + session.createNativeQuery(mql, ItemWithArrayAndCollectionValues.class) + .getSingleResult()); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq( + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .id)), + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .projectAll())); + assertEq( + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections, + session.createNativeQuery( + mql, + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .class) + .getSingleResult()); + }); + }); + } + + /** + * See Scalar + * queries, {@link QueryProducer#createNativeQuery(String, Class)}. + */ + @Test + void testScalar() { + sessionFactoryScope.inSession(session -> assertAll( + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(item.id)), + exclude(Item.projectAll(), List.of("primitiveChar", "boxedChar")))); + assertEq( + new Object[] { + item.id, + item.primitiveInt, + item.primitiveLong, + item.primitiveDouble, + item.primitiveBoolean, + item.boxedInt, + item.boxedLong, + item.boxedDouble, + item.boxedBoolean, + item.string, + item.bigDecimal, + item.objectId, + item.instant + }, + session.createNativeQuery(mql, Object[].class).getSingleResult()); + }, + () -> { + var mql = mql(COLLECTION_NAME, List.of(match(eq(item.id)), project(include("primitiveChar")))); + assertEq( + item.primitiveChar, + session.createNativeQuery(mql, char.class).getSingleResult()); + }, + () -> { + var mql = mql(COLLECTION_NAME, List.of(match(eq(item.id)), project(include("boxedChar")))); + assertEq( + item.boxedChar, + session.createNativeQuery(mql, Character.class).getSingleResult()); + }, + () -> { + var mql = mql(COLLECTION_NAME, List.of(match(eq(item.id)), project(include("objectId")))); + assertEq( + item.objectId, + session.createNativeQuery(mql, ObjectId.class).getSingleResult()); + }, + () -> { + var mql = mql( + COLLECTION_NAME, List.of(match(eq(itemWithNestedValue.id)), project(include("nested")))); + assertEq( + itemWithNestedValue.nested, + session.createNativeQuery(mql, StructAggregateEmbeddableIntegrationTests.Plural.class) + .getSingleResult()); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithNestedValueHavingArraysAndCollections.id)), + project(include("nested")))); + assertEq( + itemWithNestedValueHavingArraysAndCollections.nested, + session.createNativeQuery( + mql, StructAggregateEmbeddableIntegrationTests.ArraysAndCollections.class) + .getSingleResult()); + })); + } + + /** + * See + * Returning DTOs (Data Transfer Objects), {@link QueryProducer#createNativeQuery(String, Class)}. + */ + @Nested + class Dto { + @Test + void testBasicValues() { + sessionFactoryScope.inSession(session -> { + var mql = mql(COLLECTION_NAME, List.of(match(eq(item.id)), Item.projectAll())); + assertAll( + () -> assertEq( + item, + session.createNativeQuery(mql, Item.CONSTRUCTOR_MAPPING_FOR_ITEM, Item.class) + .getSingleResult()), + () -> assertEq( + item, + session.createNativeQuery(mql, Item.MAPPING_FOR_ITEM, Tuple.class) + .setTupleTransformer((tuple, aliases) -> new Item( + (int) tuple[0], + (char) tuple[1], + (int) tuple[2], + (long) tuple[3], + (double) tuple[4], + (boolean) tuple[5], + (Character) tuple[6], + (Integer) tuple[7], + (Long) tuple[8], + (Double) tuple[9], + (Boolean) tuple[10], + (String) tuple[11], + (BigDecimal) tuple[12], + (ObjectId) tuple[13], + (Instant) tuple[14])) + .getSingleResult())); + }); + } + + @Test + void testEmbeddableValue() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of(match(eq(itemWithFlattenedValue.id)), ItemWithFlattenedValue.projectFlattened())); + assertAll( + () -> assertEq( + itemWithFlattenedValue.flattened, + session.createNativeQuery( + mql, + ItemWithFlattenedValue.CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE, + EmbeddableIntegrationTests.Plural.class) + .getSingleResult()), + () -> assertEq( + itemWithFlattenedValue.flattened, + session.createNativeQuery( + mql, ItemWithFlattenedValue.MAPPING_FOR_FLATTENED_VALUE, Tuple.class) + .setTupleTransformer((tuple, aliases) -> new EmbeddableIntegrationTests.Plural( + (char) tuple[0], + (int) tuple[1], + (long) tuple[2], + (double) tuple[3], + (boolean) tuple[4], + (Character) tuple[5], + (Integer) tuple[6], + (Long) tuple[7], + (Double) tuple[8], + (Boolean) tuple[9], + (String) tuple[10], + (BigDecimal) tuple[11], + (ObjectId) tuple[12], + (Instant) tuple[13])) + .getSingleResult())); + }); + } + + @Test + void testEmbeddableValueHavingArraysAndCollections() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithFlattenedValueHavingArraysAndCollections.id)), + ItemWithFlattenedValueHavingArraysAndCollections.projectFlattened())); + assertAll( + () -> assertEq( + itemWithFlattenedValueHavingArraysAndCollections.flattened, + session.createNativeQuery( + mql, + ItemWithFlattenedValueHavingArraysAndCollections + .CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE, + EmbeddableIntegrationTests.ArraysAndCollections.class) + .getSingleResult()), + () -> assertEq( + itemWithFlattenedValueHavingArraysAndCollections.flattened, + session.createNativeQuery( + mql, + ItemWithFlattenedValueHavingArraysAndCollections + .MAPPING_FOR_FLATTENED_VALUE, + Tuple.class) + .setTupleTransformer((tuple, aliases) -> + new EmbeddableIntegrationTests.ArraysAndCollections( + (byte[]) tuple[0], + (char[]) tuple[1], + (int[]) tuple[2], + (long[]) tuple[3], + (double[]) tuple[4], + (boolean[]) tuple[5], + (Character[]) tuple[6], + (Integer[]) tuple[7], + (Long[]) tuple[8], + (Double[]) tuple[9], + (Boolean[]) tuple[10], + (String[]) tuple[11], + (BigDecimal[]) tuple[12], + (ObjectId[]) tuple[13], + (Instant[]) tuple[14], + (StructAggregateEmbeddableIntegrationTests.Single[]) tuple[15], + asList((Character[]) tuple[16]), + new HashSet<>(asList((Integer[]) tuple[17])), + asList((Long[]) tuple[18]), + asList((Double[]) tuple[19]), + asList((Boolean[]) tuple[20]), + asList((String[]) tuple[21]), + asList((BigDecimal[]) tuple[22]), + asList((ObjectId[]) tuple[23]), + asList((Instant[]) tuple[24]), + asList((StructAggregateEmbeddableIntegrationTests.Single[]) + tuple[25]))) + .getSingleResult())); + }); + } + + @Test + void testArrayAndCollectionValues() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithArrayAndCollectionValues.id)), + ItemWithArrayAndCollectionValues.projectAll())); + assertAll( + () -> assertEq( + itemWithArrayAndCollectionValues, + session.createNativeQuery( + mql, + ItemWithArrayAndCollectionValues.CONSTRUCTOR_MAPPING_FOR_ITEM, + ItemWithArrayAndCollectionValues.class) + .getSingleResult()), + () -> assertEq( + itemWithArrayAndCollectionValues, + session.createNativeQuery( + mql, ItemWithArrayAndCollectionValues.MAPPING_FOR_ITEM, Tuple.class) + .setTupleTransformer((tuple, aliases) -> new ItemWithArrayAndCollectionValues( + (int) tuple[0], + (byte[]) tuple[1], + (char[]) tuple[2], + (int[]) tuple[3], + (long[]) tuple[4], + (double[]) tuple[5], + (boolean[]) tuple[6], + (Character[]) tuple[7], + (Integer[]) tuple[8], + (Long[]) tuple[9], + (Double[]) tuple[10], + (Boolean[]) tuple[11], + (String[]) tuple[12], + (BigDecimal[]) tuple[13], + (ObjectId[]) tuple[14], + (Instant[]) tuple[15], + (StructAggregateEmbeddableIntegrationTests.Single[]) tuple[16], + asList((Character[]) tuple[17]), + new HashSet<>(asList((Integer[]) tuple[18])), + asList((Long[]) tuple[19]), + asList((Double[]) tuple[20]), + asList((Boolean[]) tuple[21]), + asList((String[]) tuple[22]), + asList((BigDecimal[]) tuple[23]), + asList((ObjectId[]) tuple[24]), + asList((Instant[]) tuple[25]), + asList((StructAggregateEmbeddableIntegrationTests.Single[]) tuple[26]))) + .getSingleResult())); + }); + } + + @Test + void testArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq( + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .id)), + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .projectAll())); + assertAll( + () -> assertEq( + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections, + session.createNativeQuery( + mql, + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .CONSTRUCTOR_MAPPING_FOR_ITEM, + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .class) + .getSingleResult()), + () -> assertEq( + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections, + session.createNativeQuery( + mql, + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .MAPPING_FOR_ITEM, + Tuple.class) + .setTupleTransformer((tuple, aliases) -> + new ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + (int) tuple[0], + (StructAggregateEmbeddableIntegrationTests.ArraysAndCollections + []) + tuple[1], + asList( + (StructAggregateEmbeddableIntegrationTests + .ArraysAndCollections + []) + tuple[2]))) + .getSingleResult())); + }); + } + } + + @Nested + class Unsupported { + /** + * We do not support this due to what seem to be a Hibernate ORM bug: Entity native query incorrectly handles + * AggregateSupport.preferSelectAggregateMapping that returns true. + * + * @see #testEntity() + */ + @Test + void testEntityWithAggregateEmbeddableValue() { + sessionFactoryScope.inSession(session -> { + assertAll( + () -> { + var mql = mql( + COLLECTION_NAME, + List.of(match(eq(itemWithNestedValue.id)), ItemWithNestedValue.projectAll())); + assertThatThrownBy(() -> session.createNativeQuery(mql, ItemWithNestedValue.class) + .getSingleResult()) + .hasRootCauseInstanceOf(SQLException.class) + .hasMessageContaining(MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithNestedValueHavingArraysAndCollections.id)), + ItemWithNestedValueHavingArraysAndCollections.projectAll())); + assertThatThrownBy(() -> session.createNativeQuery( + mql, ItemWithNestedValueHavingArraysAndCollections.class) + .getSingleResult()) + .hasRootCauseInstanceOf(SQLException.class) + .hasMessageContaining(MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); + }); + }); + } + } + + private static String mql(String collectionName, Iterable stages) { + var pipeline = new BsonArray(); + stages.forEach(stage -> pipeline.add(stage.toBsonDocument())); + return new BsonDocument("aggregate", new BsonString(collectionName)) + .append("pipeline", pipeline) + .toJson(EXTENDED_JSON_WRITER_SETTINGS); + } + + private static Bson exclude(Bson projectStage, Iterable fieldNames) { + var fieldsWithoutExclusions = projectStage.toBsonDocument().clone().getDocument("$project"); + var excludeId = false; + for (var fieldName : fieldNames) { + fieldsWithoutExclusions.remove(fieldName); + excludeId |= fieldName.equals(ID_FIELD_NAME); + } + return excludeId + ? project(fields(Projections.excludeId(), fieldsWithoutExclusions)) + : project(fieldsWithoutExclusions); + } + + private static Bson excludeId(Bson projectStage) { + return exclude(projectStage, singleton(ID_FIELD_NAME)); + } + + @Entity + @Table(name = COLLECTION_NAME) + @SqlResultSetMapping( + name = ItemWithFlattenedValue.CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE, + classes = + @ConstructorResult( + targetClass = EmbeddableIntegrationTests.Plural.class, + columns = { + @ColumnResult(name = "primitiveChar", type = char.class), + @ColumnResult(name = "primitiveInt", type = int.class), + @ColumnResult(name = "primitiveLong", type = long.class), + @ColumnResult(name = "primitiveDouble", type = double.class), + @ColumnResult(name = "primitiveBoolean", type = boolean.class), + @ColumnResult(name = "boxedChar", type = Character.class), + @ColumnResult(name = "boxedInt", type = Integer.class), + @ColumnResult(name = "boxedLong", type = Long.class), + @ColumnResult(name = "boxedDouble", type = Double.class), + @ColumnResult(name = "boxedBoolean", type = Boolean.class), + @ColumnResult(name = "string", type = String.class), + @ColumnResult(name = "bigDecimal", type = BigDecimal.class), + @ColumnResult(name = "objectId", type = ObjectId.class), + @ColumnResult(name = "instant", type = Instant.class), + })) + @SqlResultSetMapping( + name = ItemWithFlattenedValue.MAPPING_FOR_FLATTENED_VALUE, + columns = { + @ColumnResult(name = "primitiveChar", type = char.class), + @ColumnResult(name = "primitiveInt"), + @ColumnResult(name = "primitiveLong"), + @ColumnResult(name = "primitiveDouble"), + @ColumnResult(name = "primitiveBoolean"), + @ColumnResult(name = "boxedChar", type = Character.class), + @ColumnResult(name = "boxedInt"), + @ColumnResult(name = "boxedLong"), + @ColumnResult(name = "boxedDouble"), + @ColumnResult(name = "boxedBoolean"), + @ColumnResult(name = "string"), + @ColumnResult(name = "bigDecimal"), + @ColumnResult(name = "objectId"), + @ColumnResult(name = "instant", type = Instant.class), + }) + static class ItemWithFlattenedValue { + static final String CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE = "ConstructorFlattenedValue"; + static final String MAPPING_FOR_FLATTENED_VALUE = "FlattenedValue"; + + @Id + int id; + + EmbeddableIntegrationTests.Plural flattened; + + ItemWithFlattenedValue() {} + + ItemWithFlattenedValue(int id, EmbeddableIntegrationTests.Plural flattened) { + this.id = id; + this.flattened = flattened; + } + + static Bson projectAll() { + return BasicCrudIntegrationTests.Item.projectAll(); + } + + static Bson projectFlattened() { + return excludeId(projectAll()); + } + } + + @Entity + @Table(name = COLLECTION_NAME) + @SqlResultSetMapping( + name = ItemWithFlattenedValueHavingArraysAndCollections.CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE, + classes = + @ConstructorResult( + targetClass = EmbeddableIntegrationTests.ArraysAndCollections.class, + columns = { + @ColumnResult(name = "bytes", type = byte[].class), + @ColumnResult(name = "chars", type = char[].class), + @ColumnResult(name = "ints", type = int[].class), + @ColumnResult(name = "longs", type = long[].class), + @ColumnResult(name = "doubles", type = double[].class), + @ColumnResult(name = "booleans", type = boolean[].class), + @ColumnResult(name = "boxedChars", type = Character[].class), + @ColumnResult(name = "boxedInts", type = Integer[].class), + @ColumnResult(name = "boxedLongs", type = Long[].class), + @ColumnResult(name = "boxedDoubles", type = Double[].class), + @ColumnResult(name = "boxedBooleans", type = Boolean[].class), + @ColumnResult(name = "strings", type = String[].class), + @ColumnResult(name = "bigDecimals", type = BigDecimal[].class), + @ColumnResult(name = "objectIds", type = ObjectId[].class), + @ColumnResult(name = "instants", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddables", + type = StructAggregateEmbeddableIntegrationTests.Single[].class), + @ColumnResult(name = "charsCollection", type = Character[].class), + @ColumnResult(name = "intsCollection", type = Integer[].class), + @ColumnResult(name = "longsCollection", type = Long[].class), + @ColumnResult(name = "doublesCollection", type = Double[].class), + @ColumnResult(name = "booleansCollection", type = Boolean[].class), + @ColumnResult(name = "stringsCollection", type = String[].class), + @ColumnResult(name = "bigDecimalsCollection", type = BigDecimal[].class), + @ColumnResult(name = "objectIdsCollection", type = ObjectId[].class), + @ColumnResult(name = "instantsCollection", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddablesCollection", + type = StructAggregateEmbeddableIntegrationTests.Single[].class) + })) + @SqlResultSetMapping( + name = ItemWithFlattenedValueHavingArraysAndCollections.MAPPING_FOR_FLATTENED_VALUE, + columns = { + @ColumnResult(name = "bytes", type = byte[].class), + @ColumnResult(name = "chars", type = char[].class), + @ColumnResult(name = "ints", type = int[].class), + @ColumnResult(name = "longs", type = long[].class), + @ColumnResult(name = "doubles", type = double[].class), + @ColumnResult(name = "booleans", type = boolean[].class), + @ColumnResult(name = "boxedChars", type = Character[].class), + @ColumnResult(name = "boxedInts", type = Integer[].class), + @ColumnResult(name = "boxedLongs", type = Long[].class), + @ColumnResult(name = "boxedDoubles", type = Double[].class), + @ColumnResult(name = "boxedBooleans", type = Boolean[].class), + @ColumnResult(name = "strings", type = String[].class), + @ColumnResult(name = "bigDecimals", type = BigDecimal[].class), + @ColumnResult(name = "objectIds", type = ObjectId[].class), + @ColumnResult(name = "instants", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddables", + type = StructAggregateEmbeddableIntegrationTests.Single[].class), + @ColumnResult(name = "charsCollection", type = Character[].class), + @ColumnResult(name = "intsCollection", type = Integer[].class), + @ColumnResult(name = "longsCollection", type = Long[].class), + @ColumnResult(name = "doublesCollection", type = Double[].class), + @ColumnResult(name = "booleansCollection", type = Boolean[].class), + @ColumnResult(name = "stringsCollection", type = String[].class), + @ColumnResult(name = "bigDecimalsCollection", type = BigDecimal[].class), + @ColumnResult(name = "objectIdsCollection", type = ObjectId[].class), + @ColumnResult(name = "instantsCollection", type = Instant[].class), + @ColumnResult( + name = "structAggregateEmbeddablesCollection", + type = StructAggregateEmbeddableIntegrationTests.Single[].class) + }) + static class ItemWithFlattenedValueHavingArraysAndCollections { + static final String CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE = + "ConstructorFlattenedValueHavingArraysAndCollections"; + static final String MAPPING_FOR_FLATTENED_VALUE = "FlattenedValueHavingArraysAndCollections"; + + @Id + int id; + + EmbeddableIntegrationTests.ArraysAndCollections flattened; + + ItemWithFlattenedValueHavingArraysAndCollections() {} + + ItemWithFlattenedValueHavingArraysAndCollections( + int id, EmbeddableIntegrationTests.ArraysAndCollections flattened) { + this.id = id; + this.flattened = flattened; + } + + static Bson projectAll() { + return ArrayAndCollectionIntegrationTests.ItemWithArrayAndCollectionValues.projectAll(); + } + + static Bson projectFlattened() { + return excludeId(projectAll()); + } + } + + @Entity + @Table(name = COLLECTION_NAME) + static class ItemWithNestedValue { + @Id + int id; + + StructAggregateEmbeddableIntegrationTests.Plural nested; + + ItemWithNestedValue() {} + + ItemWithNestedValue(int id, StructAggregateEmbeddableIntegrationTests.Plural nested) { + this.id = id; + this.nested = nested; + } + + static Bson projectAll() { + return project(include(ID_FIELD_NAME, "nested")); + } + } + + @Entity + @Table(name = COLLECTION_NAME) + static class ItemWithNestedValueHavingArraysAndCollections { + @Id + int id; + + StructAggregateEmbeddableIntegrationTests.ArraysAndCollections nested; + + ItemWithNestedValueHavingArraysAndCollections() {} + + ItemWithNestedValueHavingArraysAndCollections( + int id, StructAggregateEmbeddableIntegrationTests.ArraysAndCollections nested) { + this.id = id; + this.nested = nested; + } + + static Bson projectAll() { + return project(include(ID_FIELD_NAME, "nested")); + } + } +} 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 2cb15c29..52c91fcb 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -16,10 +16,13 @@ package com.mongodb.hibernate.query.select; +import static com.mongodb.hibernate.BasicCrudIntegrationTests.Item.COLLECTION_NAME; import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static org.assertj.core.api.Assertions.assertThatCode; +import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; import com.mongodb.hibernate.query.Book; import jakarta.persistence.Entity; @@ -37,8 +40,21 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -@DomainModel(annotatedClasses = {SimpleSelectQueryIntegrationTests.Contact.class, Book.class}) +@DomainModel( + annotatedClasses = { + SimpleSelectQueryIntegrationTests.Contact.class, + Book.class, + SimpleSelectQueryIntegrationTests.Unsupported.ItemWithNestedValue.class + }) class SimpleSelectQueryIntegrationTests extends AbstractQueryIntegrationTests { + @Test + void testQueryPlanCacheIsSupported() { + getSessionFactoryScope().inTransaction(session -> assertThatCode( + () -> session.createSelectionQuery("from Contact", Contact.class) + .setQueryPlanCacheable(true) + .getResultList()) + .doesNotThrowAnyException()); + } @Nested class QueryTests { @@ -643,7 +659,7 @@ void testProjectUsingWrongAlias() { } @Nested - class FeatureNotSupportedTests { + class Unsupported { @Test void testComparisonBetweenFieldAndNonValueNotSupported1() { assertSelectQueryFailure( @@ -702,12 +718,36 @@ void testNullParameterNotSupported() { } @Test - void testQueryPlanCacheIsSupported() { - getSessionFactoryScope().inTransaction(session -> assertThatCode( - () -> session.createSelectionQuery("from Contact", Contact.class) - .setQueryPlanCacheable(true) - .getResultList()) - .doesNotThrowAnyException()); + void testStructAggregateEmbeddablePathExpressionSelection() { + assertSelectQueryFailure( + "select nested.a from ItemWithNestedValue", + int.class, + FeatureNotSupportedException.class, + MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); + } + + @Test + void testStructAggregateEmbeddablePathExpressionComparison() { + assertSelectQueryFailure( + "from ItemWithNestedValue where nested.a = 0", + ItemWithNestedValue.class, + FeatureNotSupportedException.class, + MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); + } + + @Entity(name = "ItemWithNestedValue") + @Table(name = COLLECTION_NAME) + static class ItemWithNestedValue { + @Id + int id; + + StructAggregateEmbeddableIntegrationTests.Single nested; + + ItemWithNestedValue() {} + + ItemWithNestedValue(StructAggregateEmbeddableIntegrationTests.Single nested) { + this.nested = nested; + } } } 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 913a2b05..e4ec10e2 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -21,11 +21,12 @@ import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.hibernate.cfg.QuerySettings.DEFAULT_NULL_ORDERING; +import static org.hibernate.cfg.AvailableSettings.DEFAULT_NULL_ORDERING; import static org.hibernate.query.NullPrecedence.NONE; import static org.hibernate.query.SortDirection.ASCENDING; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; import com.mongodb.hibernate.query.Book; import java.util.Arrays; @@ -40,7 +41,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -@DomainModel(annotatedClasses = Book.class) +@DomainModel(annotatedClasses = {Book.class, SimpleSelectQueryIntegrationTests.Unsupported.ItemWithNestedValue.class}) class SortingSelectQueryIntegrationTests extends AbstractQueryIntegrationTests { private static final List testingBooks = List.of( @@ -275,7 +276,7 @@ void testDefaultNullPrecedenceFeatureNotSupported() { } @Nested - class UnsupportedTests { + class Unsupported { @Test void testSortFieldNotFieldPathExpressionNotSupported() { assertSelectQueryFailure( @@ -309,6 +310,15 @@ void testCaseInsensitiveSortSpecNotSupported() { .hasMessage("TODO-HIBERNATE-79 https://jira.mongodb.org/browse/HIBERNATE-79"); }); } + + @Test + void testStructAggregateEmbeddablePathExpressionSorting() { + assertSelectQueryFailure( + "from ItemWithNestedValue order by nested.a", + SimpleSelectQueryIntegrationTests.Unsupported.ItemWithNestedValue.class, + FeatureNotSupportedException.class, + MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); + } } @Nested diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index d8aa1665..f6c1db3b 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -21,13 +21,13 @@ import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.Sealed; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; import com.mongodb.hibernate.internal.dialect.function.array.MongoArrayConstructorFunction; import com.mongodb.hibernate.internal.dialect.function.array.MongoArrayContainsFunction; import com.mongodb.hibernate.internal.dialect.function.array.MongoArrayIncludesFunction; 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; @@ -131,7 +131,7 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv private void contributeObjectIdType(TypeContributions typeContributions) { typeContributions.contributeJavaType(ObjectIdJavaType.INSTANCE); typeContributions.contributeJdbcType(ObjectIdJdbcType.INSTANCE); - var objectIdTypeCode = MqlType.OBJECT_ID.getVendorTypeNumber(); + var objectIdTypeCode = ObjectIdJdbcType.SQL_TYPE.getVendorTypeNumber(); typeContributions .getTypeConfiguration() .getDdlTypeRegistry() diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java index 444709cd..1e37e5e8 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java @@ -27,6 +27,6 @@ private MongoConstants() {} JsonWriterSettings.builder().outputMode(JsonMode.EXTENDED).build(); public static final String MONGO_DBMS_NAME = "MongoDB"; - public static final String MONGO_JDBC_DRIVER_NAME = "MongoDB Java Driver JDBC Adapter"; + public static final String MONGO_JDBC_DRIVER_NAME = MONGO_DBMS_NAME + " Java Driver JDBC Adapter"; public static final String ID_FIELD_NAME = "_id"; } diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java b/src/main/java/com/mongodb/hibernate/internal/dialect/MongoAggregateSupport.java similarity index 63% rename from src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java rename to src/main/java/com/mongodb/hibernate/internal/dialect/MongoAggregateSupport.java index 37183a3b..53af28c8 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java +++ b/src/main/java/com/mongodb/hibernate/internal/dialect/MongoAggregateSupport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mongodb.hibernate.dialect; +package com.mongodb.hibernate.internal.dialect; import static java.lang.String.format; @@ -25,8 +25,10 @@ import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.Column; -final class MongoAggregateSupport extends AggregateSupportImpl { - static final MongoAggregateSupport INSTANCE = new MongoAggregateSupport(); +public final class MongoAggregateSupport extends AggregateSupportImpl { + public static final MongoAggregateSupport INSTANCE = new MongoAggregateSupport(); + public static final String UNSUPPORTED_MESSAGE_PREFIX = + "TODO-HIBERNATE-93 https://jira.mongodb.org/browse/HIBERNATE-93"; private MongoAggregateSupport() {} @@ -41,9 +43,16 @@ public String aggregateComponentCustomReadExpression( var aggregateColumnType = aggregateColumn.getTypeCode(); if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() || aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { - return format( - "unused from %s.aggregateComponentCustomReadExpression for SQL type code [%d]", - MongoAggregateSupport.class.getSimpleName(), aggregateColumnType); + return """ + %s: [%s.%s].\ + This string is generated by %s.aggregateComponentCustomReadExpression\ + for SQL type code [%d]""" + .formatted( + UNSUPPORTED_MESSAGE_PREFIX, + aggregateParentReadExpression, + columnExpression, + MongoAggregateSupport.class.getSimpleName(), + aggregateColumnType); } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType)); } @@ -57,9 +66,16 @@ public String aggregateComponentAssignmentExpression( var aggregateColumnType = aggregateColumn.getTypeCode(); if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() || aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { - return format( - "unused from %s.aggregateComponentAssignmentExpression for SQL type code [%d]", - MongoAggregateSupport.class.getSimpleName(), aggregateColumnType); + return """ + %s: [%s.%s].\ + This string is generated by %s.aggregateComponentAssignmentExpression\ + for SQL type code [%d]""" + .formatted( + UNSUPPORTED_MESSAGE_PREFIX, + aggregateParentAssignmentExpression, + columnExpression, + MongoAggregateSupport.class.getSimpleName(), + aggregateColumnType); } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType)); } @@ -72,4 +88,10 @@ public boolean requiresAggregateCustomWriteExpressionRenderer(int aggregateSqlTy } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateSqlTypeCode)); } + + public static void checkSupported(String mql) { + if (mql.contains(UNSUPPORTED_MESSAGE_PREFIX)) { + throw new FeatureNotSupportedException(UNSUPPORTED_MESSAGE_PREFIX); + } + } } 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 3dea4aff..2d383196 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java @@ -25,6 +25,7 @@ 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 static org.hibernate.dialect.StructHelper.instantiate; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import java.io.IOException; @@ -41,6 +42,7 @@ import java.sql.Struct; import org.bson.BsonDocument; import org.bson.BsonValue; +import org.hibernate.dialect.StructAttributeValues; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.type.descriptor.ValueBinder; @@ -261,10 +263,21 @@ public MongoStructJdbcType getJdbcType() { @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(getJdbcType().extractJdbcValues(bsonDocument, options)); + var jdbcValues = getJdbcType().extractJdbcValues(bsonDocument, options); + var classX = getJavaType().getJavaTypeClass(); + Object result; + if (classX.equals(Object[].class) || jdbcValues == null) { + result = jdbcValues; + } else { + var embeddableMappingType = getEmbeddableMappingType(); + assertTrue(classX.equals(embeddableMappingType.getJavaType().getJavaTypeClass())); + result = instantiate( + embeddableMappingType, + new StructAttributeValues(jdbcValues.length, jdbcValues), + options.getSessionFactory()); + } + return classX.cast(result); } @Override diff --git a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java index c3486c78..d1be09e8 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/MqlType.java @@ -28,7 +28,7 @@ import java.util.function.ToIntFunction; import org.hibernate.type.SqlTypes; -public enum MqlType implements SQLType { +enum MqlType implements SQLType { OBJECT_ID(11_000); static { 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 91be6aa8..637fb1f2 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -23,6 +23,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLType; import org.bson.types.ObjectId; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; @@ -39,19 +40,19 @@ public final class ObjectIdJdbcType implements JdbcType { private static final long serialVersionUID = 1L; public static final ObjectIdJdbcType INSTANCE = new ObjectIdJdbcType(); - public static final MqlType MQL_TYPE = MqlType.OBJECT_ID; + public static final SQLType SQL_TYPE = MqlType.OBJECT_ID; private static final ObjectIdJavaType JAVA_TYPE = ObjectIdJavaType.INSTANCE; private ObjectIdJdbcType() {} @Override public int getJdbcTypeCode() { - return MQL_TYPE.getVendorTypeNumber(); + return SQL_TYPE.getVendorTypeNumber(); } @Override public String getFriendlyName() { - return MQL_TYPE.getName(); + return SQL_TYPE.getName(); } @Override 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 fe6388cb..9a22f04b 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ValueConversions.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ValueConversions.java @@ -22,6 +22,7 @@ import static java.lang.String.format; import com.mongodb.hibernate.internal.jdbc.MongoArray; +import jakarta.persistence.SqlResultSetMapping; import java.lang.reflect.Array; import java.math.BigDecimal; import java.sql.JDBCType; @@ -192,7 +193,7 @@ private static BsonArray arrayToBsonValue(Object value) throws SQLFeatureNotSupp } else if (value instanceof BsonDecimal128 v) { return toDomainValue(v); } else if (value instanceof BsonString v) { - return toDomainValue(v, domainType); + return uncheckedToDomainValue(v, domainType); } else if (value instanceof BsonBinary v) { return toDomainValue(v); } else if (value instanceof BsonObjectId v) { @@ -202,9 +203,11 @@ private static BsonArray arrayToBsonValue(Object value) throws SQLFeatureNotSupp } 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)); + throw exceptionDomainTypeUnsupportedOrMustBeExplicit(value, domainType); + } + + public static @Nullable Object toDomainValue(BsonValue value) throws SQLFeatureNotSupportedException { + return toDomainValue(value, UnknownDomainClass.class); } public static boolean isNull(@Nullable Object value) { @@ -256,10 +259,18 @@ private static BigDecimal toDomainValue(BsonDecimal128 value) { } public static String toStringDomainValue(BsonValue value) throws SQLFeatureNotSupportedException { - return toDomainValue(value.asString(), String.class); + return uncheckedToDomainValue(value.asString(), String.class); } - private static T toDomainValue(BsonString value, Class domainType) throws SQLFeatureNotSupportedException { + /** + * The caller must treat the result as {@link Object} if {@code T} is {@link UnknownDomainClass}. + * + * @param This method treats {@code T} as if it is {@link Object} when {@code T} is {@link UnknownDomainClass}, + * which does not cause troubles at runtime as long as the caller of the method also treats the result of the + * method as {@link Object}. + */ + private static T uncheckedToDomainValue(BsonString value, Class domainType) + throws SQLFeatureNotSupportedException { Object result; if (domainType.equals(char[].class)) { result = toDomainValue(value); @@ -267,20 +278,24 @@ private static T toDomainValue(BsonString value, Class domainType) throws var v = value.getValue(); if (domainType.equals(Character.class) && v.length() == 1) { result = toDomainValue(v); - } else if (domainType.equals(String.class) || domainType.equals(Object.class)) { + } else if (domainType.equals(String.class) || domainType.equals(UnknownDomainClass.class)) { result = v; } else { - throw new SQLFeatureNotSupportedException(format( - "Value [%s] of type [%s] is not supported for the domain type [%s]", - value, value.getClass().getTypeName(), domainType)); + throw exceptionDomainTypeUnsupportedOrMustBeExplicit(value, domainType); } } + if (domainType.equals(UnknownDomainClass.class)) { + @SuppressWarnings("unchecked") + // see the documentation of the current method + var tResult = (T) result; + return tResult; + } return domainType.cast(result); } /** @see #toBsonValue(char[]) */ private static char[] toDomainValue(BsonString value) throws SQLFeatureNotSupportedException { - return toDomainValue(value, String.class).toCharArray(); + return uncheckedToDomainValue(value, String.class).toCharArray(); } /** @see #toBsonValue(char) */ @@ -317,16 +332,39 @@ private static Instant toDomainValue(BsonDateTime value) { } public static MongoArray toArrayDomainValue(BsonValue value) throws SQLFeatureNotSupportedException { - return new MongoArray(toDomainValue(value.asArray(), Object.class)); + return new MongoArray(toDomainValue(value.asArray(), UnknownDomainClass.class)); } private static Object toDomainValue(BsonArray value, Class elementType) throws SQLFeatureNotSupportedException { var size = value.size(); - var result = Array.newInstance(elementType, size); + var elementTypeForArrayInstantiation = + elementType.equals(UnknownDomainClass.class) ? Object.class : elementType; + var result = Array.newInstance(elementTypeForArrayInstantiation, size); for (var i = 0; i < size; i++) { var element = toDomainValue(value.get(i), elementType); Array.set(result, i, element); } return result; } + + private static SQLFeatureNotSupportedException exceptionDomainTypeUnsupportedOrMustBeExplicit( + BsonValue value, Class domainType) { + var valueTypeName = value.getClass().getTypeName(); + var domainTypeName = domainType.getTypeName(); + var message = domainType.equals(UnknownDomainClass.class) + ? format( + "Value [%s] of type [%s] is either not supported or requires an explicit Java type," + + " which may be specified, for example, via %s", + value, valueTypeName, SqlResultSetMapping.class.getSimpleName()) + : format( + "Value [%s] of type [%s] is not supported for the Java type [%s]", + value, valueTypeName, domainTypeName); + return new SQLFeatureNotSupportedException(message); + } + + private static class UnknownDomainClass { + private UnknownDomainClass() { + fail(); + } + } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index d2147712..d6a2eacb 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -24,6 +24,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; import com.mongodb.hibernate.internal.type.MongoStructJdbcType; import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; import java.math.BigDecimal; @@ -56,6 +57,7 @@ final class MongoPreparedStatement extends MongoStatement implements PreparedSta MongoDatabase mongoDatabase, ClientSession clientSession, MongoConnection mongoConnection, String mql) throws SQLSyntaxErrorException { super(mongoDatabase, clientSession, mongoConnection); + MongoAggregateSupport.checkSupported(mql); this.command = MongoStatement.parse(mql); this.parameterValueSetters = new ArrayList<>(); parseParameters(command, parameterValueSetters); @@ -161,10 +163,10 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQ checkClosed(); checkParameterIndex(parameterIndex); BsonValue value; - if (targetSqlType == ObjectIdJdbcType.MQL_TYPE.getVendorTypeNumber()) { - value = toBsonValue(assertInstanceOf(x, ObjectId.class)); - } else if (targetSqlType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { + if (targetSqlType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { value = assertInstanceOf(x, BsonDocument.class); + } else if (targetSqlType == ObjectIdJdbcType.SQL_TYPE.getVendorTypeNumber()) { + value = toBsonValue(assertInstanceOf(x, ObjectId.class)); } else if (targetSqlType == JDBCType.TIMESTAMP_WITH_TIMEZONE.getVendorTypeNumber() && x instanceof Instant instant) { value = toBsonValue(instant); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index 115e17d7..a42052f3 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -40,6 +40,7 @@ import com.mongodb.hibernate.internal.type.ValueConversions; import java.math.BigDecimal; import java.sql.Array; +import java.sql.JDBCType; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -160,20 +161,27 @@ public double getDouble(int columnIndex) throws SQLException { return getValue(columnIndex, ValueConversions::toArrayDomainValue); } + @Override + public @Nullable Object getObject(int columnIndex) throws SQLException { + checkClosed(); + checkColumnIndex(columnIndex); + return getValue(columnIndex, bsonValue -> assertNotNull(ValueConversions.toDomainValue(bsonValue))); + } + @Override public @Nullable T getObject(int columnIndex, Class type) throws SQLException { checkClosed(); checkColumnIndex(columnIndex); Object value; - if (type.equals(ObjectId.class)) { - value = getValue(columnIndex, ValueConversions::toObjectIdDomainValue); - } else if (type.equals(BsonDocument.class)) { + if (type.equals(BsonDocument.class)) { value = getValue(columnIndex, ValueConversions::toBsonDocumentDomainValue); + } else if (type.equals(ObjectId.class)) { + value = getValue(columnIndex, ValueConversions::toObjectIdDomainValue); } else if (type.equals(Instant.class)) { value = getValue(columnIndex, ValueConversions::toInstantDomainValue); } else { - throw new SQLFeatureNotSupportedException( - format("Type [%s] for a column with index [%d] is not supported", type, columnIndex)); + throw new SQLFeatureNotSupportedException(format( + "Type [%s] for the column with index [%d] is not supported", type.getTypeName(), columnIndex)); } return type.cast(value); } @@ -187,7 +195,17 @@ public ResultSetMetaData getMetaData() throws SQLException { @Override public int findColumn(String columnLabel) throws SQLException { checkClosed(); - throw new SQLFeatureNotSupportedException("To be implemented in scope of native query tickets"); + // Hibernate ORM calls this method once per `columnLabel` for a given instance of `MongoResultSet`, + // which is why not having an index for `fieldNames` seems fine, + // assuming that the number of columns is not too large. + // If we ever introduce an index, it should be built lazily, because whether the `findColumn` method + // is going to be called or not is not known in advance. + for (int i = 0; i < fieldNames.size(); i++) { + if (fieldNames.get(i).equals(columnLabel)) { + return i + 1; + } + } + throw new SQLException(format("Unknown column label [%s]", columnLabel)); } @Override @@ -211,6 +229,10 @@ private T getValue(int columnIndex, SqlFunction toJavaConverte return Objects.requireNonNullElse(getValue(columnIndex, toJavaConverter), defaultValue); } + /** + * @param toJavaConverter A {@linkplain ValueConversions#isNull(Object) null value} is never passed to the + * {@link SqlFunction#apply(Object)} method of {@code toJavaConverter}. + */ private @Nullable T getValue(int columnIndex, SqlFunction toJavaConverter) throws SQLException { try { var key = getKey(columnIndex); @@ -231,7 +253,48 @@ private void checkColumnIndex(int columnIndex) throws SQLException { } } - private static final class MongoResultSetMetadata implements ResultSetMetaDataAdapter {} + private final class MongoResultSetMetadata implements ResultSetMetaDataAdapter { + private static final int SIZE_PRECISION_SCALE_NOT_APPLICABLE = 0; + + @Override + public int getColumnCount() { + return fieldNames.size(); + } + + @Override + public int getColumnDisplaySize(int column) { + return SIZE_PRECISION_SCALE_NOT_APPLICABLE; + } + + @Override + public String getColumnLabel(int column) { + return getKey(column); + } + + @Override + public int getPrecision(int column) { + return SIZE_PRECISION_SCALE_NOT_APPLICABLE; + } + + @Override + public int getScale(int column) { + return SIZE_PRECISION_SCALE_NOT_APPLICABLE; + } + + @Override + public int getColumnType(int column) { + // Hibernate ORM calls this method once per `column` for a given instance of `MongoResultSet`, + // which is why inferring the type based on the fetched data is not an option in principle: + // if the value of the `column` in the first row happens to be BSON `Null`, + // then we cannot infer the actual type. + return JDBCType.OTHER.getVendorTypeNumber(); + } + + @Override + public String getColumnTypeName(int column) { + return JDBCType.OTHER.getName(); + } + } private interface SqlFunction { R apply(T t) throws SQLException; diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index 2442a4e8..03c8ae66 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -25,6 +25,7 @@ import com.mongodb.client.MongoDatabase; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.VisibleForTesting; +import com.mongodb.hibernate.internal.dialect.MongoAggregateSupport; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -57,6 +58,7 @@ class MongoStatement implements StatementAdapter { public ResultSet executeQuery(String mql) throws SQLException { checkClosed(); closeLastOpenResultSet(); + MongoAggregateSupport.checkSupported(mql); var command = parse(mql); return executeQueryCommand(command); } @@ -121,6 +123,7 @@ private static boolean isExcludeProjectSpecification(Map.Entry