From 6fb9e8c642b1e9b0916f720261e9f2e08c84b6cf Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Tue, 2 Sep 2025 09:25:42 -0600 Subject: [PATCH 1/7] HIBERNATE-60 --- .../ArrayAndCollectionIntegrationTests.java | 110 ++- .../hibernate/BasicCrudIntegrationTests.java | 99 ++- .../EmbeddableIntegrationTests.java | 12 +- ...ctAggregateEmbeddableIntegrationTests.java | 8 +- .../query/AbstractQueryIntegrationTests.java | 2 +- ...imitOffsetFetchClauseIntegrationTests.java | 4 +- .../select/NativeQueryIntegrationTests.java | 799 ++++++++++++++++++ .../SortingSelectQueryIntegrationTests.java | 2 +- .../dialect/MongoAggregateSupport.java | 18 +- .../hibernate/dialect/MongoDialect.java | 3 +- .../hibernate/internal/MongoConstants.java | 2 +- .../internal/type/MongoStructJdbcType.java | 19 +- .../hibernate/internal/type/MqlType.java | 2 +- .../internal/type/ObjectIdJdbcType.java | 3 +- .../internal/type/ValueConversions.java | 64 +- .../hibernate/jdbc/MongoResultSet.java | 71 +- .../hibernate/jdbc/MongoStatement.java | 8 +- .../hibernate/jdbc/ResultSetAdapter.java | 2 +- 18 files changed, 1148 insertions(+), 80 deletions(-) create mode 100644 src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java 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"); } From ebf7051e8f2f94d23dd202ded7782c077909490b Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Tue, 14 Oct 2025 09:27:13 -0600 Subject: [PATCH 2/7] Replace a Zulip link with a Hibernate ORM bug report link HIBERNATE-60 --- .../hibernate/query/select/NativeQueryIntegrationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java index 8ed9833b..30b32ae9 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -566,8 +566,8 @@ void testArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndColl 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. + * href="https://hibernate.atlassian.net/browse/HHH-19866">Entity native query incorrectly handles + * AggregateSupport.preferSelectAggregateMapping that returns true. * * @see #testEntity() */ From 71c0d636e3db9f145ef47515504ab6a182cdc068 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 20 Oct 2025 21:16:30 -0600 Subject: [PATCH 3/7] Rename `ObjectIdJdbcType.MQL_TYPE` to `SQL_TYPE` HIBERNATE-60 --- .../java/com/mongodb/hibernate/dialect/MongoDialect.java | 2 +- .../mongodb/hibernate/internal/type/ObjectIdJdbcType.java | 6 +++--- .../com/mongodb/hibernate/jdbc/MongoPreparedStatement.java | 2 +- .../mongodb/hibernate/jdbc/MongoPreparedStatementTests.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index 96e08d48..8f4effe2 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -118,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 = ObjectIdJdbcType.MQL_TYPE.getVendorTypeNumber(); + var objectIdTypeCode = ObjectIdJdbcType.SQL_TYPE.getVendorTypeNumber(); typeContributions .getTypeConfiguration() .getDdlTypeRegistry() 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 d83505c1..637fb1f2 100644 --- a/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java +++ b/src/main/java/com/mongodb/hibernate/internal/type/ObjectIdJdbcType.java @@ -40,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 SQLType 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/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 20c4ec07..258611ef 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -185,7 +185,7 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQ checkClosed(); checkParameterIndex(parameterIndex); BsonValue value; - if (targetSqlType == ObjectIdJdbcType.MQL_TYPE.getVendorTypeNumber()) { + if (targetSqlType == ObjectIdJdbcType.SQL_TYPE.getVendorTypeNumber()) { value = toBsonValue(assertInstanceOf(x, ObjectId.class)); } else if (targetSqlType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber()) { value = assertInstanceOf(x, BsonDocument.class); diff --git a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java index be8f84ac..b67879fc 100644 --- a/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java +++ b/src/test/java/com/mongodb/hibernate/jdbc/MongoPreparedStatementTests.java @@ -125,8 +125,8 @@ void testSuccess() throws SQLException { preparedStatement.setInt(3, 1); preparedStatement.setBoolean(4, true); preparedStatement.setString(5, "array element"); - preparedStatement.setObject(6, new ObjectId(1, 2), ObjectIdJdbcType.MQL_TYPE.getVendorTypeNumber()); - preparedStatement.setObject(7, new ObjectId(2, 0), ObjectIdJdbcType.MQL_TYPE.getVendorTypeNumber()); + preparedStatement.setObject(6, new ObjectId(1, 2), ObjectIdJdbcType.SQL_TYPE.getVendorTypeNumber()); + preparedStatement.setObject(7, new ObjectId(2, 0), ObjectIdJdbcType.SQL_TYPE.getVendorTypeNumber()); preparedStatement.executeUpdate(); From c78dcde2f55981a96e281f36db901bd17aa8f36a Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Tue, 21 Oct 2025 22:02:15 -0600 Subject: [PATCH 4/7] Add tests, refactor them Co-authored-by: Viacheslav Babanin --- .../ArrayAndCollectionIntegrationTests.java | 145 +++++- .../hibernate/BasicCrudIntegrationTests.java | 24 + .../EmbeddableIntegrationTests.java | 78 +++- .../select/NativeQueryIntegrationTests.java | 439 +++++++++++------- 4 files changed, 503 insertions(+), 183 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java index de66c1eb..3856adab 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/ArrayAndCollectionIntegrationTests.java @@ -18,6 +18,7 @@ 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; @@ -34,6 +35,7 @@ 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; @@ -256,8 +258,31 @@ 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); + 1, + 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); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( """ @@ -493,6 +518,42 @@ private static void assertCollectionContainsExactly(String documentAsJsonObject) /** @see BasicCrudIntegrationTests.Item */ @Entity @Table(name = COLLECTION_NAME) + @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 = "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) + })) @SqlResultSetMapping( name = ItemWithArrayAndCollectionValues.MAPPING_FOR_ITEM, columns = { @@ -527,6 +588,7 @@ private static void assertCollectionContainsExactly(String documentAsJsonObject) 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 @@ -612,6 +674,60 @@ public ItemWithArrayAndCollectionValues( 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, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables, + Character[] charsCollection, + Integer[] intsCollection, + Long[] longsCollection, + Double[] doublesCollection, + Boolean[] booleansCollection, + String[] stringsCollection, + BigDecimal[] bigDecimalsCollection, + ObjectId[] objectIdsCollection, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddablesCollection) { + this( + id, + bytes, + chars, + ints, + longs, + doubles, + booleans, + boxedChars, + boxedInts, + boxedLongs, + boxedDoubles, + boxedBooleans, + strings, + bigDecimals, + objectIds, + 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), + structAggregateEmbeddablesCollection == null ? null : asList(structAggregateEmbeddablesCollection)); + } + public static Bson projectAll() { return project(include( ID_FIELD_NAME, @@ -644,6 +760,22 @@ public static Bson projectAll() { @Entity @Table(name = COLLECTION_NAME) + @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 @@ -654,6 +786,8 @@ public static Bson projectAll() { @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"; @@ -674,6 +808,13 @@ public ItemWithArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysA 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")); diff --git a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java index 42dd8d85..8a760340 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/BasicCrudIntegrationTests.java @@ -18,6 +18,7 @@ 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; @@ -29,6 +30,7 @@ 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; @@ -366,6 +368,27 @@ private static void assertCollectionContainsExactly(String documentAsJsonObject) */ @Entity @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) + })) @SqlResultSetMapping( name = MAPPING_FOR_ITEM, columns = { @@ -386,6 +409,7 @@ private static void assertCollectionContainsExactly(String documentAsJsonObject) }) 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"; @Id diff --git a/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java index 6a948070..25a3b317 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/embeddable/EmbeddableIntegrationTests.java @@ -415,8 +415,30 @@ 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, + (List) null, + null, + null, + null, + null, + null, + null, + null, + null); var item = new ItemWithFlattenedValueHavingArraysAndCollections(1, emptyEmbeddable); sessionFactoryScope.inTransaction(session -> session.persist(item)); assertCollectionContainsExactly( @@ -694,6 +716,58 @@ public ArraysAndCollections( this.objectIdsCollection = objectIdsCollection; 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, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddables, + Character[] charsCollection, + Integer[] intsCollection, + Long[] longsCollection, + Double[] doublesCollection, + Boolean[] booleansCollection, + String[] stringsCollection, + BigDecimal[] bigDecimalsCollection, + ObjectId[] objectIdsCollection, + StructAggregateEmbeddableIntegrationTests.Single[] structAggregateEmbeddablesCollection) { + this( + bytes, + chars, + ints, + longs, + doubles, + booleans, + boxedChars, + boxedInts, + boxedLongs, + boxedDoubles, + boxedBooleans, + strings, + bigDecimals, + objectIds, + 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), + structAggregateEmbeddablesCollection == null ? null : asList(structAggregateEmbeddablesCollection)); + } } @Nested diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java index 30b32ae9..350a5396 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -41,6 +41,7 @@ import com.mongodb.hibernate.embeddable.StructAggregateEmbeddableIntegrationTests; 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; @@ -320,12 +321,6 @@ void testEntity() { @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, @@ -348,6 +343,44 @@ void testScalar() { item.objectId }, 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()); })); } @@ -362,25 +395,30 @@ class Dto { 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()); + 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])) + .getSingleResult())); }); } @@ -390,24 +428,33 @@ void testEmbeddableValue() { 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()); + 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])) + .getSingleResult())); }); } @@ -419,74 +466,50 @@ void testEmbeddableValueHavingArraysAndCollections() { 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()); + 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], + (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())); }); } @@ -498,36 +521,45 @@ void testArrayAndCollectionValues() { 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()); + 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], + (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())); }); } @@ -542,22 +574,35 @@ void testArrayAndCollectionValuesOfStructAggregateEmbeddablesHavingArraysAndColl .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()); + 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())); }); } } @@ -634,6 +679,26 @@ private static Bson excludeId(Bson projectStage) { @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) + })) @SqlResultSetMapping( name = ItemWithFlattenedValue.MAPPING_FOR_FLATTENED_VALUE, columns = { @@ -652,6 +717,7 @@ private static Bson excludeId(Bson projectStage) { @ColumnResult(name = "objectId") }) static class ItemWithFlattenedValue { + static final String CONSTRUCTOR_MAPPING_FOR_FLATTENED_VALUE = "ConstructorFlattenedValue"; static final String MAPPING_FOR_FLATTENED_VALUE = "FlattenedValue"; @Id @@ -677,6 +743,41 @@ static Bson projectFlattened() { @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 = "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) + })) @SqlResultSetMapping( name = ItemWithFlattenedValueHavingArraysAndCollections.MAPPING_FOR_FLATTENED_VALUE, columns = { @@ -710,6 +811,8 @@ static Bson projectFlattened() { 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 @@ -736,12 +839,7 @@ static Bson projectFlattened() { @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; @@ -757,24 +855,11 @@ static class ItemWithNestedValue { 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; @@ -791,9 +876,5 @@ static class ItemWithNestedValueHavingArraysAndCollections { static Bson projectAll() { return project(include(ID_FIELD_NAME, "nested")); } - - static Bson projectNested() { - return excludeId(projectAll()); - } } } From 942a1774e8c95219f9ef12b08830048dcbabae36 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Tue, 21 Oct 2025 22:14:12 -0600 Subject: [PATCH 5/7] Use the technique of supreme people --- .../hibernate/query/select/NativeQueryIntegrationTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java index 350a5396..e0cb9253 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -664,9 +664,7 @@ private static Bson exclude(Bson projectStage, Iterable fieldNames) { var excludeId = false; for (var fieldName : fieldNames) { fieldsWithoutExclusions.remove(fieldName); - if (fieldName.equals(ID_FIELD_NAME)) { - excludeId = true; - } + excludeId |= fieldName.equals(ID_FIELD_NAME); } return excludeId ? project(fields(Projections.excludeId(), fieldsWithoutExclusions)) From 5bc7050b4744478a681430c5a486f75dcd5e4951 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 27 Oct 2025 10:25:19 -0600 Subject: [PATCH 6/7] Remove garbage from `NativeQueryIntegrationTests.Unsupported.testEntityWithAggregateEmbeddableValue` HIBERNATE-60 --- .../select/NativeQueryIntegrationTests.java | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java index e0cb9253..b546e679 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -252,6 +252,8 @@ void beforeEach() { * See Entity * queries, {@link QueryProducer#createNativeQuery(String, Class)}. + * + * @see Unsupported#testEntityWithAggregateEmbeddableValue() */ @Test void testEntity() { @@ -620,33 +622,27 @@ class Unsupported { 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")); + () -> { + var mql = mql( + COLLECTION_NAME, + List.of(match(eq(itemWithNestedValue.id)), ItemWithNestedValue.projectAll())); + assertThatThrownBy(() -> session.createNativeQuery(mql, ItemWithNestedValue.class) + .getSingleResult()) + .hasRootCauseInstanceOf(SQLException.class) + .hasMessageContaining("Not supported"); + }, + () -> { + var mql = mql( + COLLECTION_NAME, + List.of( + match(eq(itemWithNestedValueHavingArraysAndCollections.id)), + ItemWithNestedValueHavingArraysAndCollections.projectAll())); + assertThatThrownBy(() -> session.createNativeQuery( + mql, ItemWithNestedValueHavingArraysAndCollections.class) + .getSingleResult()) + .hasRootCauseInstanceOf(SQLException.class) + .hasMessageContaining("Not supported"); + }); }); } } From 42eed1df7e064f1ab17d7005177bc411806da34a Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Mon, 27 Oct 2025 17:36:39 -0600 Subject: [PATCH 7/7] Explicitly fail when path expressions for struct aggregate embeddables are used --- .../mutation/UpdatingIntegrationTests.java | 33 ++++++++++- ...imitOffsetFetchClauseIntegrationTests.java | 2 +- .../select/NativeQueryIntegrationTests.java | 5 +- .../SimpleSelectQueryIntegrationTests.java | 56 ++++++++++++++++--- .../SortingSelectQueryIntegrationTests.java | 14 ++++- .../hibernate/dialect/MongoDialect.java | 1 + .../dialect/MongoAggregateSupport.java | 50 ++++++++++------- .../jdbc/MongoPreparedStatement.java | 2 + .../hibernate/jdbc/MongoStatement.java | 5 ++ 9 files changed, 135 insertions(+), 33 deletions(-) rename src/main/java/com/mongodb/hibernate/{ => internal}/dialect/MongoAggregateSupport.java (63%) 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 13d1f9b2..54082750 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -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 index 036e5981..1bdaf42f 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NativeQueryIntegrationTests.java @@ -39,6 +39,7 @@ 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; @@ -646,7 +647,7 @@ void testEntityWithAggregateEmbeddableValue() { assertThatThrownBy(() -> session.createNativeQuery(mql, ItemWithNestedValue.class) .getSingleResult()) .hasRootCauseInstanceOf(SQLException.class) - .hasMessageContaining("Not supported"); + .hasMessageContaining(MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); }, () -> { var mql = mql( @@ -658,7 +659,7 @@ void testEntityWithAggregateEmbeddableValue() { mql, ItemWithNestedValueHavingArraysAndCollections.class) .getSingleResult()) .hasRootCauseInstanceOf(SQLException.class) - .hasMessageContaining("Not supported"); + .hasMessageContaining(MongoAggregateSupport.UNSUPPORTED_MESSAGE_PREFIX); }); }); } 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 6ac2e52c..e4ec10e2 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -26,6 +26,7 @@ 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 b0ff40ee..f6c1db3b 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -21,6 +21,7 @@ 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; 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 3223087c..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,14 +43,16 @@ public String aggregateComponentCustomReadExpression( var aggregateColumnType = aggregateColumn.getTypeCode(); if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() || aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { - return format( - "Not supported: [%s.%s]." - + " This string is generated by %s.aggregateComponentCustomReadExpression" - + " for SQL type code [%d]", - aggregateParentReadExpression, - columnExpression, - 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)); } @@ -62,14 +66,16 @@ public String aggregateComponentAssignmentExpression( var aggregateColumnType = aggregateColumn.getTypeCode(); if (aggregateColumnType == MongoStructJdbcType.JDBC_TYPE.getVendorTypeNumber() || aggregateColumnType == MongoArrayJdbcType.HIBERNATE_SQL_TYPE) { - return format( - "Not supported: [%s.%s]." - + " This string is generated by %s.aggregateComponentAssignmentExpression" - + " for SQL type code [%d]", - aggregateParentAssignmentExpression, - columnExpression, - 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)); } @@ -82,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/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 459081f2..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); diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoStatement.java index cc420302..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