diff --git a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java index 271d58aa..de66c1eb 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java @@ -16,7 +16,11 @@ 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.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,16 +33,19 @@ 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.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.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; @@ -64,8 +71,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; @@ -98,7 +103,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), @@ -310,8 +315,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), @@ -486,11 +490,47 @@ 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 = 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 = "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 = "structAggregateEmbeddablesCollection", + type = StructAggregateEmbeddableIntegrationTests.Single[].class) + }) + public static class ItemWithArrayAndCollectionValues { + public static final String MAPPING_FOR_ITEM = "ItemWithArrayAndCollectionValues"; + @Id - int id; + public int id; byte[] bytes; char[] chars; @@ -519,7 +559,7 @@ static class ItemWithArrayAndCollectionValues { ItemWithArrayAndCollectionValues() {} - ItemWithArrayAndCollectionValues( + public ItemWithArrayAndCollectionValues( int id, byte[] bytes, char[] chars, @@ -571,20 +611,61 @@ static class ItemWithArrayAndCollectionValues { this.objectIdsCollection = objectIdsCollection; this.structAggregateEmbeddablesCollection = 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", + "structAggregateEmbeddables", + "charsCollection", + "intsCollection", + "longsCollection", + "doublesCollection", + "booleansCollection", + "stringsCollection", + "bigDecimalsCollection", + "objectIdsCollection", + "structAggregateEmbeddablesCollection")); + } } @Entity @Table(name = COLLECTION_NAME) - static class ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections { + @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 MAPPING_FOR_ITEM = + "ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections"; + @Id - int id; + public int id; ArraysAndCollections[] structAggregateEmbeddables; Collection structAggregateEmbeddablesCollection; ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections() {} - ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( + public ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections( int id, ArraysAndCollections[] structAggregateEmbeddables, Collection structAggregateEmbeddablesCollection) { @@ -592,6 +673,11 @@ static class ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingA this.structAggregateEmbeddables = structAggregateEmbeddables; this.structAggregateEmbeddablesCollection = 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 63242746..42dd8d85 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java @@ -16,17 +16,26 @@ 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.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.Entity; import jakarta.persistence.Id; +import jakarta.persistence.SqlResultSetMapping; import jakarta.persistence.Table; import java.math.BigDecimal; 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; @@ -43,10 +52,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; @@ -348,29 +356,58 @@ 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 = 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") + }) + public static class Item { + public static final String COLLECTION_NAME = "items"; + 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; + @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; Item() {} - Item( + public Item( int id, char primitiveChar, int primitiveInt, @@ -400,10 +437,28 @@ static class Item { this.bigDecimal = bigDecimal; this.objectId = objectId; } + + public static Bson projectAll() { + return project(include( + ID_FIELD_NAME, + "primitiveChar", + "primitiveInt", + "primitiveLong", + "primitiveDouble", + "primitiveBoolean", + "boxedChar", + "boxedInt", + "boxedLong", + "boxedDouble", + "boxedBoolean", + "string", + "bigDecimal", + "objectId")); + } } @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 2e468886..6a948070 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; @@ -63,8 +65,6 @@ }) @ExtendWith(MongoExtension.class) public class EmbeddableIntegrationTests implements SessionFactoryScopeAware { - private static final String COLLECTION_NAME = "items"; - @InjectMongoCollection(COLLECTION_NAME) private static MongoCollection mongoCollection; @@ -581,8 +581,9 @@ ItemWithFlattenedValues getParent() { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable - record Plural( + public record Plural( char primitiveChar, int primitiveInt, long primitiveLong, @@ -613,8 +614,9 @@ static class ItemWithFlattenedValueHavingArraysAndCollections { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable - static class ArraysAndCollections { + public static class ArraysAndCollections { byte[] bytes; char[] chars; int[] ints; @@ -642,7 +644,7 @@ static class ArraysAndCollections { ArraysAndCollections() {} - ArraysAndCollections( + public ArraysAndCollections( byte[] bytes, char[] chars, int[] ints, diff --git a/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/embeddable/StructAggregateEmbeddableIntegrationTests.java index cf4699f2..5808efc1 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; @@ -62,8 +64,6 @@ }) @ExtendWith(MongoExtension.class) public class StructAggregateEmbeddableIntegrationTests implements SessionFactoryScopeAware { - private static final String COLLECTION_NAME = "items"; - @InjectMongoCollection(COLLECTION_NAME) private static MongoCollection mongoCollection; @@ -578,9 +578,10 @@ ItemWithNestedValues getParent() { } } + /** @see BasicCrudIntegrationTests.Item */ @Embeddable @Struct(name = "Plural") - record Plural( + public record Plural( char primitiveChar, int primitiveInt, long primitiveLong, @@ -611,6 +612,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 19ee1e81..4de7080e 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/select/LimitOffsetFetchClauseIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java index c70e9544..d24cdcb5 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.AssertionsForClassTypes.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; 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..8ed9833b --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -0,0 +1,799 @@ +/* + * 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.junit.MongoExtension; +import jakarta.persistence.ColumnResult; +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.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")); + 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"))); + 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 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(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"))); + 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 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(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 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(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)}. + */ + @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)), project(include("objectId")))); + assertEq( + item.objectId, + session.createNativeQuery(mql, ObjectId.class).getSingleResult()); + }, + () -> { + 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 + }, + session.createNativeQuery(mql, Object[].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())); + 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])) + .getSingleResult()); + }); + } + + @Test + void testEmbeddableValue() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of(match(eq(itemWithFlattenedValue.id)), ItemWithFlattenedValue.projectFlattened())); + 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])) + .getSingleResult()); + }); + } + + @Test + void testEmbeddableValueHavingArraysAndCollections() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithFlattenedValueHavingArraysAndCollections.id)), + ItemWithFlattenedValueHavingArraysAndCollections.projectFlattened())); + 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], + (StructAggregateEmbeddableIntegrationTests.Single[]) tuple[14], + asList((Character[]) tuple[15]), + new HashSet<>(asList((Integer[]) tuple[16])), + asList((Long[]) tuple[17]), + asList((Double[]) tuple[18]), + asList((Boolean[]) tuple[19]), + asList((String[]) tuple[20]), + asList((BigDecimal[]) tuple[21]), + asList((ObjectId[]) tuple[22]), + asList((StructAggregateEmbeddableIntegrationTests.Single[]) tuple[23]))) + .getSingleResult()); + }); + } + + @Test + void testStructAggregateEmbeddableValue() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of(match(eq(itemWithNestedValue.id)), ItemWithNestedValue.projectNested())); + assertEq( + itemWithNestedValue.nested, + session.createNativeQuery(mql, ItemWithNestedValue.MAPPING_FOR_NESTED_VALUE, Tuple.class) + .setTupleTransformer( + (tuple, aliases) -> (StructAggregateEmbeddableIntegrationTests.Plural) tuple[0]) + .getSingleResult()); + }); + } + + @Test + void testStructAggregateEmbeddableValueHavingArraysAndCollections() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithNestedValueHavingArraysAndCollections.id)), + ItemWithNestedValueHavingArraysAndCollections.projectNested())); + assertEq( + itemWithNestedValueHavingArraysAndCollections.nested, + session.createNativeQuery( + mql, + ItemWithNestedValueHavingArraysAndCollections.MAPPING_FOR_NESTED_VALUE, + Tuple.class) + .setTupleTransformer((tuple, aliases) -> + (StructAggregateEmbeddableIntegrationTests.ArraysAndCollections) tuple[0]) + .getSingleResult()); + }); + } + + @Test + void testArrayAndCollectionValues() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithArrayAndCollectionValues.id)), + ItemWithArrayAndCollectionValues.projectAll())); + 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], + (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((StructAggregateEmbeddableIntegrationTests.Single[]) tuple[24]))) + .getSingleResult()); + }); + } + + @Test + void testArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections() { + sessionFactoryScope.inSession(session -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq( + itemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .id)), + ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndCollections + .projectAll())); + 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: + * Issue with an entity native query when there is a struct. + * + * @see #testEntity() + */ + @Test + void testEntityWithAggregateEmbeddableValue() { + sessionFactoryScope.inSession(session -> { + assertAll( + () -> assertThatThrownBy(() -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithNestedValue.id)), + ItemWithNestedValue.projectAll())); + assertEq( + itemWithFlattenedValue, + session.createNativeQuery(mql, ItemWithNestedValue.class) + .getSingleResult()); + }) + .hasRootCauseInstanceOf(SQLException.class) + .hasMessageContaining("Not supported"), + () -> assertThatThrownBy(() -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithNestedValueHavingArraysAndCollections.id)), + ItemWithNestedValueHavingArraysAndCollections.projectAll())); + assertEq( + itemWithFlattenedValueHavingArraysAndCollections, + session.createNativeQuery( + mql, ItemWithNestedValueHavingArraysAndCollections.class) + .getSingleResult()); + }) + .hasRootCauseInstanceOf(SQLException.class) + .hasMessageContaining("Not supported")); + }); + } + } + + 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); + if (fieldName.equals(ID_FIELD_NAME)) { + excludeId = true; + } + } + 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.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") + }) + static class ItemWithFlattenedValue { + 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.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 = "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 = "structAggregateEmbeddablesCollection", + type = StructAggregateEmbeddableIntegrationTests.Single[].class) + }) + static class ItemWithFlattenedValueHavingArraysAndCollections { + 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) + @SqlResultSetMapping( + name = ItemWithNestedValue.MAPPING_FOR_NESTED_VALUE, + columns = {@ColumnResult(name = "nested", type = StructAggregateEmbeddableIntegrationTests.Plural.class)}) + static class ItemWithNestedValue { + static final String MAPPING_FOR_NESTED_VALUE = "NestedValue"; + + @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")); + } + + static Bson projectNested() { + return excludeId(projectAll()); + } + } + + @Entity + @Table(name = COLLECTION_NAME) + @SqlResultSetMapping( + name = ItemWithNestedValueHavingArraysAndCollections.MAPPING_FOR_NESTED_VALUE, + columns = { + @ColumnResult( + name = "nested", + type = StructAggregateEmbeddableIntegrationTests.ArraysAndCollections.class) + }) + static class ItemWithNestedValueHavingArraysAndCollections { + static final String MAPPING_FOR_NESTED_VALUE = "NestedValueHavingArraysAndCollections"; + + @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")); + } + + static Bson projectNested() { + return excludeId(projectAll()); + } + } +} 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 080e71fc..3456ffec 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -20,7 +20,7 @@ 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; diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java b/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java index 37183a3b..3223087c 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoAggregateSupport.java @@ -42,8 +42,13 @@ public String aggregateComponentCustomReadExpression( 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); + "Not supported: [%s.%s]." + + " This string is generated by %s.aggregateComponentCustomReadExpression" + + " for SQL type code [%d]", + aggregateParentReadExpression, + columnExpression, + MongoAggregateSupport.class.getSimpleName(), + aggregateColumnType); } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType)); } @@ -58,8 +63,13 @@ public String aggregateComponentAssignmentExpression( 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); + "Not supported: [%s.%s]." + + " This string is generated by %s.aggregateComponentAssignmentExpression" + + " for SQL type code [%d]", + aggregateParentAssignmentExpression, + columnExpression, + MongoAggregateSupport.class.getSimpleName(), + aggregateColumnType); } throw new FeatureNotSupportedException(format("The SQL type code [%d] is not supported", aggregateColumnType)); } diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index 12ea1286..96e08d48 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -26,7 +26,6 @@ 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; @@ -119,7 +118,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.MQL_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/internal/type/MongoStructJdbcType.java b/src/main/java/com/mongodb/hibernate/internal/type/MongoStructJdbcType.java index c09ee055..e05e6a03 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.Serial; @@ -38,6 +39,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; @@ -252,10 +254,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..d83505c1 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,7 +40,7 @@ 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 MQL_TYPE = MqlType.OBJECT_ID; private static final ObjectIdJavaType JAVA_TYPE = ObjectIdJavaType.INSTANCE; private ObjectIdJdbcType() {} 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 9b7ee446..ca0017d9 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; @@ -184,7 +185,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) { @@ -192,9 +193,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) { @@ -246,10 +249,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); @@ -257,14 +268,18 @@ 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); } @@ -294,12 +309,14 @@ private static ObjectId toDomainValue(BsonObjectId 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); @@ -309,6 +326,27 @@ private static Object toDomainValue(BsonArray value, Class elementType) throw /** @see #toBsonValue(char[]) */ private static char[] toDomainValue(BsonString value) throws SQLFeatureNotSupportedException { - return toDomainValue(value, String.class).toCharArray(); + return uncheckedToDomainValue(value, String.class).toCharArray(); + } + + 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/MongoResultSet.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java index 7df76973..54ef7c17 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoResultSet.java @@ -41,6 +41,7 @@ import java.math.BigDecimal; import java.sql.Array; import java.sql.Date; +import java.sql.JDBCType; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -198,6 +199,13 @@ 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(); @@ -208,8 +216,8 @@ public double getDouble(int columnIndex) throws SQLException { } else if (type.equals(BsonDocument.class)) { value = getValue(columnIndex, ValueConversions::toBsonDocumentDomainValue); } 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); } @@ -223,7 +231,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 @@ -247,6 +265,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); @@ -267,7 +289,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..cc420302 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java @@ -165,25 +165,25 @@ public void clearWarnings() throws SQLException { public boolean execute(String mql) throws SQLException { checkClosed(); closeLastOpenResultSet(); - throw new SQLFeatureNotSupportedException("To be implemented in scope of index and unique constraint creation"); + throw new SQLFeatureNotSupportedException("TODO-HIBERNATE-66 https://jira.mongodb.org/browse/HIBERNATE-66"); } @Override public @Nullable ResultSet getResultSet() throws SQLException { checkClosed(); - throw new SQLFeatureNotSupportedException("To be implemented in scope of index and unique constraint creation"); + throw new SQLFeatureNotSupportedException("TODO-HIBERNATE-66 https://jira.mongodb.org/browse/HIBERNATE-66"); } @Override public boolean getMoreResults() throws SQLException { checkClosed(); - throw new SQLFeatureNotSupportedException("To be implemented in scope of index and unique constraint creation"); + throw new SQLFeatureNotSupportedException("TODO-HIBERNATE-66 https://jira.mongodb.org/browse/HIBERNATE-66"); } @Override public int getUpdateCount() throws SQLException { checkClosed(); - throw new SQLFeatureNotSupportedException("To be implemented in scope of index and unique constraint creation"); + throw new SQLFeatureNotSupportedException("TODO-HIBERNATE-66 https://jira.mongodb.org/browse/HIBERNATE-66"); } @Override diff --git a/src/main/java/com/mongodb/hibernate/jdbc/ResultSetAdapter.java b/src/main/java/com/mongodb/hibernate/jdbc/ResultSetAdapter.java index dcd7b912..b69ab0a8 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/ResultSetAdapter.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/ResultSetAdapter.java @@ -241,7 +241,7 @@ default ResultSetMetaData getMetaData() throws SQLException { } @Override - default Object getObject(int columnIndex) throws SQLException { + default @Nullable Object getObject(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("getObject not implemented"); }