From e8971468414273d1cb1076563d8884017e4eacff Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 3 Apr 2025 10:01:11 -0400 Subject: [PATCH 01/50] sorting MQL translation --- ...bstractSelectionQueryIntegrationTests.java | 110 +++++++++ .../mongodb/hibernate/query/select/Book.java | 29 ++- .../SimpleSelectQueryIntegrationTests.java | 82 +------ .../query/select/SortingIntegrationTests.java | 211 ++++++++++++++++++ .../hibernate/dialect/MongoDialect.java | 5 + .../hibernate/internal/MongoConstants.java | 2 + .../translate/AbstractMqlTranslator.java | 78 +++++-- .../command/aggregate/AstSortField.java | 28 +++ .../command/aggregate/AstSortOrder.java | 32 +++ .../command/aggregate/AstSortStage.java | 43 ++++ .../command/aggregate/AstSortFieldTests.java | 36 +++ .../command/aggregate/AstSortOrderTests.java | 36 +++ .../command/aggregate/AstSortStageTests.java | 37 +++ 13 files changed, 628 insertions(+), 101 deletions(-) create mode 100644 src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java create mode 100644 src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStage.java create mode 100644 src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java create mode 100644 src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java create mode 100644 src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStageTests.java diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java new file mode 100644 index 00000000..b42aa1f9 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java @@ -0,0 +1,110 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mongodb.hibernate.TestCommandListener; +import com.mongodb.hibernate.junit.MongoExtension; +import java.util.List; +import java.util.function.Consumer; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.bson.BsonDocument; +import org.hibernate.query.SelectionQuery; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.hibernate.testing.orm.junit.ServiceRegistryScopeAware; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.extension.ExtendWith; + +@SessionFactory(exportSchema = false) +@ExtendWith(MongoExtension.class) +abstract class AbstractSelectionQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { + + SessionFactoryScope sessionFactoryScope; + + TestCommandListener testCommandListener; + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Override + public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope) { + this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class); + } + + void assertSelectionQuery( + String hql, + Class resultType, + Consumer> queryPostProcessor, + String expectedMql, + List expectedResultList) { + assertSelectionQuery(hql, resultType, queryPostProcessor, expectedMql, resultList -> assertThat(resultList) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyElementsOf(expectedResultList)); + } + + void assertSelectionQuery( + String hql, + Class resultType, + Consumer> queryPostProcessor, + String expectedMql, + Consumer> resultListVerifier) { + sessionFactoryScope.inTransaction(session -> { + var selectionQuery = session.createSelectionQuery(hql, resultType); + if (queryPostProcessor != null) { + queryPostProcessor.accept(selectionQuery); + } + var resultList = selectionQuery.getResultList(); + + assertActualCommand(BsonDocument.parse(expectedMql)); + + resultListVerifier.accept(resultList); + }); + } + + void assertSelectQueryFailure( + String hql, + Class resultType, + Consumer> queryPostProcessor, + Class expectedExceptionType, + String expectedExceptionMessage, + Object... expectedExceptionMessageParameters) { + sessionFactoryScope.inTransaction(session -> assertThatThrownBy(() -> { + var selectionQuery = session.createSelectionQuery(hql, resultType); + if (queryPostProcessor != null) { + queryPostProcessor.accept(selectionQuery); + } + selectionQuery.getResultList(); + }) + .isInstanceOf(expectedExceptionType) + .hasMessage(expectedExceptionMessage, expectedExceptionMessageParameters)); + } + + void assertActualCommand(BsonDocument expectedCommand) { + var capturedCommands = testCommandListener.getStartedCommands(); + + assertThat(capturedCommands) + .singleElement() + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsAllEntriesOf(expectedCommand); + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java index 82e8ef62..42e4fe39 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java @@ -16,21 +16,16 @@ package com.mongodb.hibernate.query.select; -import com.mongodb.hibernate.annotations.ObjectIdGenerator; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.math.BigDecimal; -import org.bson.types.ObjectId; @Entity(name = "Book") @Table(name = "books") -public class Book { +class Book { @Id - @ObjectIdGenerator - ObjectId id; - - public Book() {} + int id; String title; Boolean outOfStock; @@ -38,4 +33,24 @@ public Book() {} Long isbn13; Double discount; BigDecimal price; + + Book() {} + + Book(int id, String title, Integer publishYear, Boolean outOfStock) { + this.id = id; + this.title = title; + this.publishYear = publishYear; + this.outOfStock = outOfStock; + + // the following fields are set dummy values + // for currently null value is not supported + this.isbn13 = 0L; + this.discount = 0.0; + this.price = BigDecimal.valueOf(0); + } + + @Override + public String toString() { + return "Book{" + "id=" + id + '}'; + } } 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 42a86e76..ed9123d5 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -17,55 +17,25 @@ package com.mongodb.hibernate.query.select; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.mongodb.hibernate.TestCommandListener; import com.mongodb.hibernate.internal.FeatureNotSupportedException; -import com.mongodb.hibernate.junit.MongoExtension; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; -import java.util.function.Consumer; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.bson.BsonDocument; -import org.hibernate.query.SelectionQuery; import org.hibernate.query.SemanticException; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.ServiceRegistryScope; -import org.hibernate.testing.orm.junit.ServiceRegistryScopeAware; -import org.hibernate.testing.orm.junit.SessionFactory; -import org.hibernate.testing.orm.junit.SessionFactoryScope; -import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -@SessionFactory(exportSchema = false) @DomainModel(annotatedClasses = {SimpleSelectQueryIntegrationTests.Contact.class, Book.class}) -@ExtendWith(MongoExtension.class) -class SimpleSelectQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { - - private SessionFactoryScope sessionFactoryScope; - - private TestCommandListener testCommandListener; - - @Override - public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { - this.sessionFactoryScope = sessionFactoryScope; - } - - @Override - public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope) { - this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class); - } +class SimpleSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { @Nested class QueryTests { @@ -82,7 +52,7 @@ private static List getTestingContacts(int... ids) { .mapToObj(id -> testingContacts.stream() .filter(c -> c.id == id) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("id not exists: " + id))) + .orElseThrow(() -> new IllegalArgumentException("id does not exist: " + id))) .toList(); } @@ -413,54 +383,6 @@ void testBigDecimal() { } } - private void assertSelectionQuery( - String hql, - Class resultType, - Consumer> queryPostProcessor, - String expectedMql, - List expectedResultList) { - sessionFactoryScope.inTransaction(session -> { - var selectionQuery = session.createSelectionQuery(hql, resultType); - if (queryPostProcessor != null) { - queryPostProcessor.accept(selectionQuery); - } - var resultList = selectionQuery.getResultList(); - - assertActualCommand(BsonDocument.parse(expectedMql)); - - assertThat(resultList) - .usingRecursiveFieldByFieldElementComparator() - .containsExactlyElementsOf(expectedResultList); - }); - } - - private void assertSelectQueryFailure( - String hql, - Class resultType, - Consumer> queryPostProcessor, - Class expectedExceptionType, - String expectedExceptionMessage, - Object... expectedExceptionMessageParameters) { - sessionFactoryScope.inTransaction(session -> assertThatThrownBy(() -> { - var selectionQuery = session.createSelectionQuery(hql, resultType); - if (queryPostProcessor != null) { - queryPostProcessor.accept(selectionQuery); - } - selectionQuery.getResultList(); - }) - .isInstanceOf(expectedExceptionType) - .hasMessage(expectedExceptionMessage, expectedExceptionMessageParameters)); - } - - private void assertActualCommand(BsonDocument expectedCommand) { - var capturedCommands = testCommandListener.getStartedCommands(); - - assertThat(capturedCommands) - .singleElement() - .asInstanceOf(InstanceOfAssertFactories.MAP) - .containsAllEntriesOf(expectedCommand); - } - @Entity(name = "Contact") @Table(name = "contacts") static class Contact { diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java new file mode 100644 index 00000000..3da58648 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -0,0 +1,211 @@ +/* + * 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.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; +import static org.assertj.core.api.Assertions.assertThat; + +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.MongoConstants; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Arrays; +import java.util.List; +import org.hibernate.testing.orm.junit.DomainModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +@DomainModel(annotatedClasses = {Book.class, SortingIntegrationTests.EntityWithTooManyFields.class}) +class SortingIntegrationTests extends AbstractSelectionQueryIntegrationTests { + + private static final List BOOKS = List.of( + new Book(1, "War and Peace", 1869, true), + new Book(2, "Crime and Punishment", 1866, false), + new Book(3, "Anna Karenina", 1877, false), + new Book(4, "The Brothers Karamazov", 1880, false), + new Book(5, "War and Peace", 2025, false)); + + private static List getBooksByIds(int... ids) { + return Arrays.stream(ids) + .mapToObj(id -> BOOKS.stream() + .filter(c -> c.id == id) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("id does not exist: " + id))) + .toList(); + } + + @BeforeEach + void beforeEach() { + sessionFactoryScope.inTransaction(session -> BOOKS.forEach(session::persist)); + testCommandListener.clear(); + } + + @ParameterizedTest + @EnumSource(AstSortOrder.class) + void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { + assertSelectionQuery( + "from Book as b order by b.publishYear " + sortOrder, + Book.class, + null, + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + + sortOrder.getRenderedValue() + + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", + sortOrder == AstSortOrder.ASC ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); + } + + @ParameterizedTest + @EnumSource(AstSortOrder.class) + void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { + assertSelectionQuery( + "from Book as b order by b.title " + sortOrder, + Book.class, + null, + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + sortOrder.getRenderedValue() + + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", + sortOrder == AstSortOrder.ASC + ? resultList -> assertThat(resultList) + .satisfiesAnyOf( + list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 1, 5)), + list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 5, 1))) + : resultList -> assertThat(resultList) + .satisfiesAnyOf( + list -> assertResultList(resultList, getBooksByIds(1, 5, 4, 2, 3)), + list -> assertResultList(resultList, getBooksByIds(5, 1, 4, 2, 3)))); + } + + @Test + void testOrderByMultipleFieldsWithoutTies() { + assertSelectionQuery( + "from Book where outOfStock = false order by title ASC, publishYear DESC, id ASC", + Book.class, + null, + "{ 'aggregate': 'books', 'pipeline': [ {'$match': {'outOfStock': {'$eq': false}}}, { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", + getBooksByIds(3, 2, 4, 5)); + } + + @Test + void testOrderByMultipleFieldsWithTies() { + assertSelectionQuery( + "from Book order by title ASC, publishYear DESC, id ASC", + Book.class, + null, + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", + resultList -> assertThat(resultList) + .satisfiesAnyOf( + list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 1, 5)), + list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 5, 1)))); + } + + @Test + void testSortFieldByAlias() { + assertSelectionQuery( + "select b.title as title, b.publishYear as year from Book as b order by year desc, title asc", + Object[].class, + null, + "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", + List.of( + new Object[] {"War and Peace", 2025}, + new Object[] {"The Brothers Karamazov", 1880}, + new Object[] {"Anna Karenina", 1877}, + new Object[] {"War and Peace", 1869}, + new Object[] {"Crime and Punishment", 1866})); + } + + @Test + void testTooManySortFieldsThrowsException() { + assertSelectQueryFailure( + "from EntityWithTooManyFields order by f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16,f17,f18,f19,f20,f21,f22,f23,f24,f25,f26,f27,f28,f29,f30,f31,f32,f33", + EntityWithTooManyFields.class, + null, + FeatureNotSupportedException.class, + "%s does not support more than %d sort keys", + MONGO_DBMS_NAME, + MongoConstants.SORT_KEY_MAX_NUM); + } + + @Test + void testSortFieldDuplicated() { + assertSelectQueryFailure( + "from Book order by title, publishYear, title", + Book.class, + null, + FeatureNotSupportedException.class, + "%s does not support duplicated sort keys ('%s' field is used more than once)", + MONGO_DBMS_NAME, + "title"); + } + + @Test + void testNullPrecedenceFeatureNotSupported() { + assertSelectQueryFailure( + "from Book order by publishYear nulls last", + Book.class, + null, + FeatureNotSupportedException.class, + "%s does not support Null Precedence", + MONGO_DBMS_NAME); + } + + @Entity(name = "EntityWithTooManyFields") + @Table(name = "entities") + static class EntityWithTooManyFields { + @Id + int id; + + String f1; + String f2; + String f3; + String f4; + String f5; + String f6; + String f7; + String f8; + String f9; + String f10; + String f11; + String f12; + String f13; + String f14; + String f15; + String f16; + String f17; + String f18; + String f19; + String f20; + String f21; + String f22; + String f23; + String f24; + String f25; + String f26; + String f27; + String f28; + String f29; + String f30; + String f31; + String f32; + String f33; + } + + static void assertResultList(List resultList, List expectedBooks) { + assertThat(resultList).usingRecursiveFieldByFieldElementComparator().containsExactlyElementsOf(expectedBooks); + } +} diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index d3015f9b..cddc3a60 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -97,4 +97,9 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv public @Nullable String toQuotedIdentifier(@Nullable String name) { return name; } + + @Override + public boolean supportsNullPrecedence() { + return false; + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java index e0426506..40cdeca1 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java @@ -23,4 +23,6 @@ private MongoConstants() {} 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 ID_FIELD_NAME = "_id"; + + public static final int SORT_KEY_MAX_NUM = 32; } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 3d929453..76d6b1dc 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -20,6 +20,7 @@ import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; +import static com.mongodb.hibernate.internal.MongoConstants.SORT_KEY_MAX_NUM; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME; @@ -27,6 +28,8 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; +import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; +import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.DESC; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.GT; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.GTE; @@ -55,6 +58,9 @@ import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageIncludeSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortStage; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstStage; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFieldOperationFilter; @@ -67,6 +73,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import org.bson.BsonBoolean; import org.bson.BsonDecimal128; @@ -83,6 +90,8 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.query.NullPrecedence; +import org.hibernate.query.SortDirection; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; @@ -360,28 +369,69 @@ public void visitQuerySpec(QuerySpec querySpec) { if (!querySpec.getGroupByClauseExpressions().isEmpty()) { throw new FeatureNotSupportedException("GroupBy not supported"); } - if (querySpec.hasSortSpecifications()) { - throw new FeatureNotSupportedException("Sorting not supported"); - } if (querySpec.hasOffsetOrFetchClause()) { throw new FeatureNotSupportedException("TODO-HIBERNATE-70 https://jira.mongodb.org/browse/HIBERNATE-70"); } var collection = acceptAndYield(querySpec.getFromClause(), COLLECTION_NAME); + var stages = new ArrayList(3); + + // $match stage var whereClauseRestrictions = querySpec.getWhereClauseRestrictions(); - var filter = whereClauseRestrictions == null || whereClauseRestrictions.isEmpty() - ? null - : acceptAndYield(whereClauseRestrictions, FILTER); + if (whereClauseRestrictions != null && !whereClauseRestrictions.isEmpty()) { + var filter = acceptAndYield(whereClauseRestrictions, FILTER); + stages.add(new AstMatchStage(filter)); + } + // $sort stage + getOptionalAstSortStage(querySpec).ifPresent(stages::add); + + // $project stage var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS); + stages.add(new AstProjectStage(projectStageSpecifications)); - var stages = filter == null - ? List.of(new AstProjectStage(projectStageSpecifications)) - : List.of(new AstMatchStage(filter), new AstProjectStage(projectStageSpecifications)); astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); } + private Optional getOptionalAstSortStage(QuerySpec querySpec) { + if (querySpec.hasSortSpecifications()) { + + if (querySpec.getSortSpecifications().size() > SORT_KEY_MAX_NUM) { + throw new FeatureNotSupportedException( + format("%s does not support more than %d sort keys", MONGO_DBMS_NAME, SORT_KEY_MAX_NUM)); + } + + var sortFields = new ArrayList( + querySpec.getSortSpecifications().size()); + + var encounteredSortFieldPaths = new HashSet(); + for (SortSpecification sortSpecification : querySpec.getSortSpecifications()) { + var sortFieldPath = acceptAndYield(sortSpecification.getSortExpression(), FIELD_PATH); + if (!encounteredSortFieldPaths.add(sortFieldPath)) { + throw new FeatureNotSupportedException(format( + "%s does not support duplicated sort keys ('%s' field is used more than once)", + MONGO_DBMS_NAME, sortFieldPath)); + } + + if (sortSpecification.getNullPrecedence() != null + && sortSpecification.getNullPrecedence() != NullPrecedence.NONE) { + throw new FeatureNotSupportedException( + format("%s does not support Null Precedence", MONGO_DBMS_NAME)); + } + if (sortSpecification.isIgnoreCase()) { + throw new FeatureNotSupportedException(); + } + + var sortField = new AstSortField( + sortFieldPath, sortSpecification.getSortOrder() == SortDirection.ASCENDING ? ASC : DESC); + sortFields.add(sortField); + } + return Optional.of(new AstSortStage(sortFields)); + } + return Optional.empty(); + } + @Override public void visitFromClause(FromClause fromClause) { if (fromClause.getRoots().size() != 1) { @@ -498,6 +548,11 @@ public void visitUnparsedNumericLiteral(UnparsedNumericLitera FIELD_VALUE, new AstLiteralValue(toBsonValue(unparsedNumericLiteral.getLiteralValue()))); } + @Override + public void visitSqlSelectionExpression(SqlSelectionExpression sqlSelectionExpression) { + sqlSelectionExpression.getSelection().getExpression().accept(this); + } + @Override public void visitDeleteStatement(DeleteStatement deleteStatement) { throw new FeatureNotSupportedException("TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46"); @@ -653,11 +708,6 @@ public void visitSelfRenderingExpression(SelfRenderingExpression selfRenderingEx throw new FeatureNotSupportedException(); } - @Override - public void visitSqlSelectionExpression(SqlSelectionExpression sqlSelectionExpression) { - throw new FeatureNotSupportedException(); - } - @Override public void visitEntityTypeLiteral(EntityTypeLiteral entityTypeLiteral) { throw new FeatureNotSupportedException(); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java new file mode 100644 index 00000000..e9ce90c3 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import com.mongodb.hibernate.internal.translate.mongoast.AstNode; +import org.bson.BsonWriter; + +public record AstSortField(String path, AstSortOrder order) implements AstNode { + @Override + public void render(BsonWriter writer) { + writer.writeName(path); + writer.writeInt32(order.getRenderedValue()); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java new file mode 100644 index 00000000..2831479e --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +public enum AstSortOrder { + ASC(1), + DESC(-1); + + private final int renderedValue; + + AstSortOrder(int renderedValue) { + this.renderedValue = renderedValue; + } + + public int getRenderedValue() { + return renderedValue; + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStage.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStage.java new file mode 100644 index 00000000..ad39bdd8 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStage.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import static com.mongodb.hibernate.internal.MongoAssertions.assertFalse; + +import java.util.List; +import org.bson.BsonWriter; + +public record AstSortStage(List sortFields) implements AstStage { + + public AstSortStage { + assertFalse(sortFields.isEmpty()); + } + + @Override + public void render(BsonWriter writer) { + writer.writeStartDocument(); + { + writer.writeName("$sort"); + writer.writeStartDocument(); + { + sortFields.forEach(sortField -> sortField.render(writer)); + } + writer.writeEndDocument(); + } + writer.writeEndDocument(); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java new file mode 100644 index 00000000..d09839fe --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertElementRender; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class AstSortFieldTests { + + @ParameterizedTest + @EnumSource(AstSortOrder.class) + void testRendering(AstSortOrder sortOrder) { + var sortField = new AstSortField("path", sortOrder); + var expectedJson = + """ + {"path": %d}\ + """.formatted(sortOrder.getRenderedValue()); + assertElementRender(expectedJson, sortField); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java new file mode 100644 index 00000000..b5a5f29c --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class AstSortOrderTests { + + @ParameterizedTest + @EnumSource(AstSortOrder.class) + void testRenderingValue(AstSortOrder sortOrder) { + var expectedValue = + switch (sortOrder) { + case ASC -> 1; + case DESC -> -1; + }; + assertEquals(expectedValue, sortOrder.getRenderedValue()); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStageTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStageTests.java new file mode 100644 index 00000000..930a9642 --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortStageTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AstSortStageTests { + + @Test + void testRendering() { + var astSortField1 = new AstSortField("field1", AstSortOrder.ASC); + var astSortField2 = new AstSortField("field2", AstSortOrder.DESC); + var astSortStage = new AstSortStage(List.of(astSortField1, astSortField2)); + + var expectedJson = """ + {"$sort": {"field1": 1, "field2": -1}}\ + """; + assertRender(expectedJson, astSortStage); + } +} From 6123f8f268e34421f88c58571d130e63e765251d Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Wed, 23 Apr 2025 09:28:30 -0400 Subject: [PATCH 02/50] change `Null Precedence` verbigage to `Nulls Precedence` --- .../mongodb/hibernate/query/select/SortingIntegrationTests.java | 2 +- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index 3da58648..124117a7 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -160,7 +160,7 @@ void testNullPrecedenceFeatureNotSupported() { Book.class, null, FeatureNotSupportedException.class, - "%s does not support Null Precedence", + "%s does not support Nulls Precedence", MONGO_DBMS_NAME); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 76d6b1dc..df769729 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -417,7 +417,7 @@ private Optional getOptionalAstSortStage(QuerySpec querySpec) { if (sortSpecification.getNullPrecedence() != null && sortSpecification.getNullPrecedence() != NullPrecedence.NONE) { throw new FeatureNotSupportedException( - format("%s does not support Null Precedence", MONGO_DBMS_NAME)); + format("%s does not support Nulls Precedence", MONGO_DBMS_NAME)); } if (sortSpecification.isIgnoreCase()) { throw new FeatureNotSupportedException(); From 422b98b611a0a2ed5ad2d7211bd8215f206a0502 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 24 Apr 2025 10:02:35 -0400 Subject: [PATCH 03/50] add ordinal sort field reference testing case --- .../query/select/SortingIntegrationTests.java | 15 +++++++++++++++ .../internal/translate/AbstractMqlTranslator.java | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index 124117a7..840817cc 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -129,6 +129,21 @@ void testSortFieldByAlias() { new Object[] {"Crime and Punishment", 1866})); } + @Test + void testSortFieldByOrdinalReference() { + assertSelectionQuery( + "select b.title as title, b.publishYear as year from Book as b order by 2 desc, 1 asc", + Object[].class, + null, + "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", + List.of( + new Object[] {"War and Peace", 2025}, + new Object[] {"The Brothers Karamazov", 1880}, + new Object[] {"Anna Karenina", 1877}, + new Object[] {"War and Peace", 1869}, + new Object[] {"Crime and Punishment", 1866})); + } + @Test void testTooManySortFieldsThrowsException() { assertSelectQueryFailure( diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index df769729..c7966153 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -420,7 +420,8 @@ private Optional getOptionalAstSortStage(QuerySpec querySpec) { format("%s does not support Nulls Precedence", MONGO_DBMS_NAME)); } if (sortSpecification.isIgnoreCase()) { - throw new FeatureNotSupportedException(); + throw new FeatureNotSupportedException( + format("%s does not support string sorting ignoring case", MONGO_DBMS_NAME)); } var sortField = new AstSortField( From 0e628e078d118be3b61dae757469d0a6e07bcc1b Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 24 Apr 2025 13:26:36 -0400 Subject: [PATCH 04/50] removed sort key max num validation logic --- .../query/select/SortingIntegrationTests.java | 59 +------------------ .../hibernate/internal/MongoConstants.java | 2 - .../translate/AbstractMqlTranslator.java | 7 --- 3 files changed, 1 insertion(+), 67 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index 840817cc..fffb5ca5 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -20,11 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.mongodb.hibernate.internal.FeatureNotSupportedException; -import com.mongodb.hibernate.internal.MongoConstants; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; import java.util.Arrays; import java.util.List; import org.hibernate.testing.orm.junit.DomainModel; @@ -33,7 +29,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -@DomainModel(annotatedClasses = {Book.class, SortingIntegrationTests.EntityWithTooManyFields.class}) +@DomainModel(annotatedClasses = Book.class) class SortingIntegrationTests extends AbstractSelectionQueryIntegrationTests { private static final List BOOKS = List.of( @@ -144,18 +140,6 @@ void testSortFieldByOrdinalReference() { new Object[] {"Crime and Punishment", 1866})); } - @Test - void testTooManySortFieldsThrowsException() { - assertSelectQueryFailure( - "from EntityWithTooManyFields order by f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13,f14,f15,f16,f17,f18,f19,f20,f21,f22,f23,f24,f25,f26,f27,f28,f29,f30,f31,f32,f33", - EntityWithTooManyFields.class, - null, - FeatureNotSupportedException.class, - "%s does not support more than %d sort keys", - MONGO_DBMS_NAME, - MongoConstants.SORT_KEY_MAX_NUM); - } - @Test void testSortFieldDuplicated() { assertSelectQueryFailure( @@ -179,47 +163,6 @@ void testNullPrecedenceFeatureNotSupported() { MONGO_DBMS_NAME); } - @Entity(name = "EntityWithTooManyFields") - @Table(name = "entities") - static class EntityWithTooManyFields { - @Id - int id; - - String f1; - String f2; - String f3; - String f4; - String f5; - String f6; - String f7; - String f8; - String f9; - String f10; - String f11; - String f12; - String f13; - String f14; - String f15; - String f16; - String f17; - String f18; - String f19; - String f20; - String f21; - String f22; - String f23; - String f24; - String f25; - String f26; - String f27; - String f28; - String f29; - String f30; - String f31; - String f32; - String f33; - } - static void assertResultList(List resultList, List expectedBooks) { assertThat(resultList).usingRecursiveFieldByFieldElementComparator().containsExactlyElementsOf(expectedBooks); } diff --git a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java index 40cdeca1..e0426506 100644 --- a/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java +++ b/src/main/java/com/mongodb/hibernate/internal/MongoConstants.java @@ -23,6 +23,4 @@ private MongoConstants() {} 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 ID_FIELD_NAME = "_id"; - - public static final int SORT_KEY_MAX_NUM = 32; } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index c7966153..096d5288 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -20,7 +20,6 @@ import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; import static com.mongodb.hibernate.internal.MongoConstants.ID_FIELD_NAME; import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; -import static com.mongodb.hibernate.internal.MongoConstants.SORT_KEY_MAX_NUM; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME; @@ -396,12 +395,6 @@ public void visitQuerySpec(QuerySpec querySpec) { private Optional getOptionalAstSortStage(QuerySpec querySpec) { if (querySpec.hasSortSpecifications()) { - - if (querySpec.getSortSpecifications().size() > SORT_KEY_MAX_NUM) { - throw new FeatureNotSupportedException( - format("%s does not support more than %d sort keys", MONGO_DBMS_NAME, SORT_KEY_MAX_NUM)); - } - var sortFields = new ArrayList( querySpec.getSortSpecifications().size()); From 0868af384c3c9f0546a6fde1cee68c0fbd2b8b96 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 24 Apr 2025 16:35:54 -0400 Subject: [PATCH 05/50] add logic to throw exception when sort specification does not denote a field path, and its testing case --- .../query/select/SortingIntegrationTests.java | 27 +++++++++++++------ .../translate/AbstractMqlTranslator.java | 7 +++-- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index fffb5ca5..197b3c1f 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -58,7 +58,7 @@ void beforeEach() { @EnumSource(AstSortOrder.class) void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { assertSelectionQuery( - "from Book as b order by b.publishYear " + sortOrder, + "from Book as b ORDER BY b.publishYear " + sortOrder, Book.class, null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " @@ -71,7 +71,7 @@ void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { @EnumSource(AstSortOrder.class) void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { assertSelectionQuery( - "from Book as b order by b.title " + sortOrder, + "from Book as b ORDER BY b.title " + sortOrder, Book.class, null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + sortOrder.getRenderedValue() @@ -90,7 +90,7 @@ void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { @Test void testOrderByMultipleFieldsWithoutTies() { assertSelectionQuery( - "from Book where outOfStock = false order by title ASC, publishYear DESC, id ASC", + "from Book where outOfStock = false ORDER BY title ASC, publishYear DESC, id ASC", Book.class, null, "{ 'aggregate': 'books', 'pipeline': [ {'$match': {'outOfStock': {'$eq': false}}}, { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", @@ -100,7 +100,7 @@ void testOrderByMultipleFieldsWithoutTies() { @Test void testOrderByMultipleFieldsWithTies() { assertSelectionQuery( - "from Book order by title ASC, publishYear DESC, id ASC", + "from Book ORDER BY title ASC, publishYear DESC, id ASC", Book.class, null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", @@ -113,7 +113,7 @@ void testOrderByMultipleFieldsWithTies() { @Test void testSortFieldByAlias() { assertSelectionQuery( - "select b.title as title, b.publishYear as year from Book as b order by year desc, title asc", + "select b.title as title, b.publishYear as year from Book as b ORDER BY year DESC, title ASC", Object[].class, null, "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", @@ -128,7 +128,7 @@ void testSortFieldByAlias() { @Test void testSortFieldByOrdinalReference() { assertSelectionQuery( - "select b.title as title, b.publishYear as year from Book as b order by 2 desc, 1 asc", + "select b.title as title, b.publishYear as year from Book as b ORDER BY 2 DESC, 1 ASC", Object[].class, null, "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", @@ -140,10 +140,21 @@ void testSortFieldByOrdinalReference() { new Object[] {"Crime and Punishment", 1866})); } + @Test + void testSortFieldNotFieldPathExpression() { + assertSelectQueryFailure( + "from Book b ORDER BY type(b)", + Book.class, + null, + FeatureNotSupportedException.class, + "%s does not support sort key not of field path type", + MONGO_DBMS_NAME); + } + @Test void testSortFieldDuplicated() { assertSelectQueryFailure( - "from Book order by title, publishYear, title", + "from Book ORDER BY title, publishYear, title", Book.class, null, FeatureNotSupportedException.class, @@ -155,7 +166,7 @@ void testSortFieldDuplicated() { @Test void testNullPrecedenceFeatureNotSupported() { assertSelectQueryFailure( - "from Book order by publishYear nulls last", + "from Book ORDER BY publishYear NULLS LAST", Book.class, null, FeatureNotSupportedException.class, diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 096d5288..fbc459d3 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -93,7 +93,6 @@ import org.hibernate.query.SortDirection; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.ComparisonOperator; -import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation; import org.hibernate.query.sqm.tree.expression.Conversion; import org.hibernate.sql.ast.Clause; @@ -400,6 +399,10 @@ private Optional getOptionalAstSortStage(QuerySpec querySpec) { var encounteredSortFieldPaths = new HashSet(); for (SortSpecification sortSpecification : querySpec.getSortSpecifications()) { + if (!isFieldPathExpression(sortSpecification.getSortExpression())) { + throw new FeatureNotSupportedException( + format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); + } var sortFieldPath = acceptAndYield(sortSpecification.getSortExpression(), FIELD_PATH); if (!encounteredSortFieldPaths.add(sortFieldPath)) { throw new FeatureNotSupportedException(format( @@ -908,7 +911,7 @@ private static AstComparisonFilterOperator getAstComparisonFilterOperator(Compar } private static boolean isFieldPathExpression(Expression expression) { - return expression instanceof ColumnReference || expression instanceof BasicValuedPathInterpretation; + return expression.getColumnReference() != null; } private static boolean isValueExpression(Expression expression) { From db884305967c40cf66ba03dbb66c238b764e392f Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 28 Apr 2025 11:33:55 -0400 Subject: [PATCH 06/50] remove duplicated sort key validation and testing --- .../query/select/SortingIntegrationTests.java | 14 +------ .../translate/AbstractMqlTranslator.java | 40 +++++++++---------- .../translate/AstVisitorValueDescriptor.java | 3 ++ 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index 197b3c1f..82479ff1 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -151,18 +151,6 @@ void testSortFieldNotFieldPathExpression() { MONGO_DBMS_NAME); } - @Test - void testSortFieldDuplicated() { - assertSelectQueryFailure( - "from Book ORDER BY title, publishYear, title", - Book.class, - null, - FeatureNotSupportedException.class, - "%s does not support duplicated sort keys ('%s' field is used more than once)", - MONGO_DBMS_NAME, - "title"); - } - @Test void testNullPrecedenceFeatureNotSupported() { assertSelectQueryFailure( @@ -170,7 +158,7 @@ void testNullPrecedenceFeatureNotSupported() { Book.class, null, FeatureNotSupportedException.class, - "%s does not support Nulls Precedence", + "%s does not support nulls precedence: NULLS LAST", MONGO_DBMS_NAME); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index fbc459d3..b7653c48 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -27,6 +27,7 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELD; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.DESC; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; @@ -93,6 +94,7 @@ import org.hibernate.query.SortDirection; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; import org.hibernate.query.sqm.sql.internal.SqmParameterInterpretation; import org.hibernate.query.sqm.tree.expression.Conversion; import org.hibernate.sql.ast.Clause; @@ -396,33 +398,22 @@ private Optional getOptionalAstSortStage(QuerySpec querySpec) { if (querySpec.hasSortSpecifications()) { var sortFields = new ArrayList( querySpec.getSortSpecifications().size()); - - var encounteredSortFieldPaths = new HashSet(); for (SortSpecification sortSpecification : querySpec.getSortSpecifications()) { if (!isFieldPathExpression(sortSpecification.getSortExpression())) { throw new FeatureNotSupportedException( format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); } - var sortFieldPath = acceptAndYield(sortSpecification.getSortExpression(), FIELD_PATH); - if (!encounteredSortFieldPaths.add(sortFieldPath)) { - throw new FeatureNotSupportedException(format( - "%s does not support duplicated sort keys ('%s' field is used more than once)", - MONGO_DBMS_NAME, sortFieldPath)); - } - if (sortSpecification.getNullPrecedence() != null && sortSpecification.getNullPrecedence() != NullPrecedence.NONE) { - throw new FeatureNotSupportedException( - format("%s does not support Nulls Precedence", MONGO_DBMS_NAME)); + throw new FeatureNotSupportedException(format( + "%s does not support nulls precedence: NULLS %s", + MONGO_DBMS_NAME, sortSpecification.getNullPrecedence())); } if (sortSpecification.isIgnoreCase()) { throw new FeatureNotSupportedException( - format("%s does not support string sorting ignoring case", MONGO_DBMS_NAME)); + format("%s does not support case-insensitive sort key", MONGO_DBMS_NAME)); } - - var sortField = new AstSortField( - sortFieldPath, sortSpecification.getSortOrder() == SortDirection.ASCENDING ? ASC : DESC); - sortFields.add(sortField); + sortFields.add(acceptAndYield(sortSpecification, SORT_FIELD)); } return Optional.of(new AstSortStage(sortFields)); } @@ -550,6 +541,14 @@ public void visitSqlSelectionExpression(SqlSelectionExpression sqlSelectionExpre sqlSelectionExpression.getSelection().getExpression().accept(this); } + @Override + public void visitSortSpecification(SortSpecification sortSpecification) { + var sortFieldPath = acceptAndYield(sortSpecification.getSortExpression(), FIELD_PATH); + var astSortField = new AstSortField( + sortFieldPath, sortSpecification.getSortOrder() == SortDirection.ASCENDING ? ASC : DESC); + astVisitorValueHolder.yield(SORT_FIELD, astSortField); + } + @Override public void visitDeleteStatement(DeleteStatement deleteStatement) { throw new FeatureNotSupportedException("TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46"); @@ -575,11 +574,6 @@ public void visitQueryGroup(QueryGroup queryGroup) { throw new FeatureNotSupportedException(); } - @Override - public void visitSortSpecification(SortSpecification sortSpecification) { - throw new FeatureNotSupportedException(); - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { throw new FeatureNotSupportedException(); @@ -911,7 +905,9 @@ private static AstComparisonFilterOperator getAstComparisonFilterOperator(Compar } private static boolean isFieldPathExpression(Expression expression) { - return expression.getColumnReference() != null; + return expression instanceof ColumnReference + || expression instanceof BasicValuedPathInterpretation + || expression instanceof SqlSelectionExpression; } private static boolean isValueExpression(Expression expression) { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index 399a4931..a6196ae9 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -22,6 +22,7 @@ import com.mongodb.hibernate.internal.translate.mongoast.AstValue; import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter; import java.lang.reflect.Modifier; import java.util.Collections; @@ -44,6 +45,8 @@ final class AstVisitorValueDescriptor { new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor FILTER = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor SORT_FIELD = new AstVisitorValueDescriptor<>(); + private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; static { From 9791755bfdde4b44bca6f8a6aed9e9a6a5340018 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 29 Apr 2025 14:44:40 -0400 Subject: [PATCH 07/50] improve testing code in minor details --- ...bstractSelectionQueryIntegrationTests.java | 7 ++++ .../query/select/SortingIntegrationTests.java | 32 ++++++++----------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java index b42aa1f9..bd77797f 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java @@ -107,4 +107,11 @@ void assertActualCommand(BsonDocument expectedCommand) { .asInstanceOf(InstanceOfAssertFactories.MAP) .containsAllEntriesOf(expectedCommand); } + + @SuppressWarnings("unchecked") + static void assertResultListEquals(List expectedResultList, List actualResultList) { + assertThat((List) actualResultList) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyElementsOf(expectedResultList); + } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index 82479ff1..759fabd9 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -17,6 +17,7 @@ package com.mongodb.hibernate.query.select; import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; +import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; import static org.assertj.core.api.Assertions.assertThat; import com.mongodb.hibernate.internal.FeatureNotSupportedException; @@ -32,7 +33,7 @@ @DomainModel(annotatedClasses = Book.class) class SortingIntegrationTests extends AbstractSelectionQueryIntegrationTests { - private static final List BOOKS = List.of( + private static final List testingBooks = List.of( new Book(1, "War and Peace", 1869, true), new Book(2, "Crime and Punishment", 1866, false), new Book(3, "Anna Karenina", 1877, false), @@ -41,7 +42,7 @@ class SortingIntegrationTests extends AbstractSelectionQueryIntegrationTests { private static List getBooksByIds(int... ids) { return Arrays.stream(ids) - .mapToObj(id -> BOOKS.stream() + .mapToObj(id -> testingBooks.stream() .filter(c -> c.id == id) .findFirst() .orElseThrow(() -> new IllegalArgumentException("id does not exist: " + id))) @@ -50,7 +51,7 @@ private static List getBooksByIds(int... ids) { @BeforeEach void beforeEach() { - sessionFactoryScope.inTransaction(session -> BOOKS.forEach(session::persist)); + sessionFactoryScope.inTransaction(session -> testingBooks.forEach(session::persist)); testCommandListener.clear(); } @@ -61,10 +62,9 @@ void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { "from Book as b ORDER BY b.publishYear " + sortOrder, Book.class, null, - "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " - + sortOrder.getRenderedValue() + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + sortOrder.getRenderedValue() + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", - sortOrder == AstSortOrder.ASC ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); + sortOrder == ASC ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); } @ParameterizedTest @@ -76,15 +76,15 @@ void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + sortOrder.getRenderedValue() + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", - sortOrder == AstSortOrder.ASC + sortOrder == ASC ? resultList -> assertThat(resultList) .satisfiesAnyOf( - list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 1, 5)), - list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 5, 1))) + list -> assertResultListEquals(getBooksByIds(3, 2, 4, 1, 5), list), + list -> assertResultListEquals(getBooksByIds(3, 2, 4, 5, 1), list)) : resultList -> assertThat(resultList) .satisfiesAnyOf( - list -> assertResultList(resultList, getBooksByIds(1, 5, 4, 2, 3)), - list -> assertResultList(resultList, getBooksByIds(5, 1, 4, 2, 3)))); + list -> assertResultListEquals(getBooksByIds(1, 5, 4, 2, 3), list), + list -> assertResultListEquals(getBooksByIds(5, 1, 4, 2, 3), list))); } @Test @@ -106,8 +106,8 @@ void testOrderByMultipleFieldsWithTies() { "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", resultList -> assertThat(resultList) .satisfiesAnyOf( - list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 1, 5)), - list -> assertResultList(resultList, getBooksByIds(3, 2, 4, 5, 1)))); + list -> assertResultListEquals(getBooksByIds(3, 2, 4, 1, 5), list), + list -> assertResultListEquals(getBooksByIds(3, 2, 4, 5, 1), list))); } @Test @@ -143,7 +143,7 @@ void testSortFieldByOrdinalReference() { @Test void testSortFieldNotFieldPathExpression() { assertSelectQueryFailure( - "from Book b ORDER BY type(b)", + "from Book ORDER BY length(title)", Book.class, null, FeatureNotSupportedException.class, @@ -161,8 +161,4 @@ void testNullPrecedenceFeatureNotSupported() { "%s does not support nulls precedence: NULLS LAST", MONGO_DBMS_NAME); } - - static void assertResultList(List resultList, List expectedBooks) { - assertThat(resultList).usingRecursiveFieldByFieldElementComparator().containsExactlyElementsOf(expectedBooks); - } } From d9790db5ffe120e91ebde24a9e482ccfeecd72be Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 29 Apr 2025 16:32:57 -0400 Subject: [PATCH 08/50] make AstSortOrder implement AstNode --- .../mongodb/hibernate/query/select/Book.java | 24 ++++--------- .../query/select/SortingIntegrationTests.java | 14 ++++---- .../command/aggregate/AstSortField.java | 2 +- .../command/aggregate/AstSortOrder.java | 10 ++++-- .../command/aggregate/AstSortFieldTests.java | 11 +++--- .../command/aggregate/AstSortOrderTests.java | 36 ------------------- 6 files changed, 28 insertions(+), 69 deletions(-) delete mode 100644 src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java index 42e4fe39..a1f1fc15 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java @@ -27,12 +27,13 @@ class Book { @Id int id; - String title; - Boolean outOfStock; - Integer publishYear; - Long isbn13; - Double discount; - BigDecimal price; + // dummy values are set for currently null value is not supported + String title = ""; + Boolean outOfStock = false; + Integer publishYear = 0; + Long isbn13 = 0L; + Double discount = 0.0; + BigDecimal price = new BigDecimal("0.0"); Book() {} @@ -41,16 +42,5 @@ class Book { this.title = title; this.publishYear = publishYear; this.outOfStock = outOfStock; - - // the following fields are set dummy values - // for currently null value is not supported - this.isbn13 = 0L; - this.discount = 0.0; - this.price = BigDecimal.valueOf(0); - } - - @Override - public String toString() { - return "Book{" + "id=" + id + '}'; } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java index 759fabd9..b7d71fc7 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java @@ -62,7 +62,7 @@ void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { "from Book as b ORDER BY b.publishYear " + sortOrder, Book.class, null, - "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + sortOrder.getRenderedValue() + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + (sortOrder == ASC ? "1" : "-1") + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", sortOrder == ASC ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); } @@ -74,7 +74,7 @@ void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { "from Book as b ORDER BY b.title " + sortOrder, Book.class, null, - "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + sortOrder.getRenderedValue() + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + (sortOrder == ASC ? "1" : "-1") + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", sortOrder == ASC ? resultList -> assertThat(resultList) @@ -128,16 +128,16 @@ void testSortFieldByAlias() { @Test void testSortFieldByOrdinalReference() { assertSelectionQuery( - "select b.title as title, b.publishYear as year from Book as b ORDER BY 2 DESC, 1 ASC", + "select b.title as title, b.publishYear as year from Book as b ORDER BY 1 ASC, 2 DESC", Object[].class, null, "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", List.of( - new Object[] {"War and Peace", 2025}, - new Object[] {"The Brothers Karamazov", 1880}, new Object[] {"Anna Karenina", 1877}, - new Object[] {"War and Peace", 1869}, - new Object[] {"Crime and Punishment", 1866})); + new Object[] {"Crime and Punishment", 1866}, + new Object[] {"The Brothers Karamazov", 1880}, + new Object[] {"War and Peace", 2025}, + new Object[] {"War and Peace", 1869})); } @Test diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java index e9ce90c3..f7ac0edf 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortField.java @@ -23,6 +23,6 @@ public record AstSortField(String path, AstSortOrder order) implements AstNode { @Override public void render(BsonWriter writer) { writer.writeName(path); - writer.writeInt32(order.getRenderedValue()); + order.render(writer); } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java index 2831479e..eb2d748b 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrder.java @@ -16,7 +16,10 @@ package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; -public enum AstSortOrder { +import com.mongodb.hibernate.internal.translate.mongoast.AstNode; +import org.bson.BsonWriter; + +public enum AstSortOrder implements AstNode { ASC(1), DESC(-1); @@ -26,7 +29,8 @@ public enum AstSortOrder { this.renderedValue = renderedValue; } - public int getRenderedValue() { - return renderedValue; + @Override + public void render(BsonWriter writer) { + writer.writeInt32(renderedValue); } } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java index d09839fe..51c93e84 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortFieldTests.java @@ -17,6 +17,7 @@ package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertElementRender; +import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -26,11 +27,11 @@ class AstSortFieldTests { @ParameterizedTest @EnumSource(AstSortOrder.class) void testRendering(AstSortOrder sortOrder) { - var sortField = new AstSortField("path", sortOrder); - var expectedJson = - """ - {"path": %d}\ - """.formatted(sortOrder.getRenderedValue()); + var sortField = new AstSortField("field", sortOrder); + var expectedJson = """ + {"field": %d}\ + """ + .formatted(sortOrder == ASC ? 1 : -1); assertElementRender(expectedJson, sortField); } } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java deleted file mode 100644 index b5a5f29c..00000000 --- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSortOrderTests.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2025-present MongoDB, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; - -class AstSortOrderTests { - - @ParameterizedTest - @EnumSource(AstSortOrder.class) - void testRenderingValue(AstSortOrder sortOrder) { - var expectedValue = - switch (sortOrder) { - case ASC -> 1; - case DESC -> -1; - }; - assertEquals(expectedValue, sortOrder.getRenderedValue()); - } -} From 606c537a3b3a610e82658908255776a5486f36d1 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 2 May 2025 13:25:52 -0400 Subject: [PATCH 09/50] resolve code review comments --- ...bstractSelectionQueryIntegrationTests.java | 15 +++ ...> SortingSelectQueryIntegrationTests.java} | 29 ++++- .../translate/AbstractMqlTranslator.java | 100 ++++++++++++------ .../translate/AstVisitorValueDescriptor.java | 4 +- 4 files changed, 111 insertions(+), 37 deletions(-) rename src/integrationTest/java/com/mongodb/hibernate/query/select/{SortingIntegrationTests.java => SortingSelectQueryIntegrationTests.java} (86%) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java index bd77797f..e389583f 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java @@ -99,6 +99,21 @@ void assertSelectQueryFailure( .hasMessage(expectedExceptionMessage, expectedExceptionMessageParameters)); } + void assertSelectQueryFailure( + String hql, + Class resultType, + Class expectedExceptionType, + String expectedExceptionMessage, + Object... expectedExceptionMessageParameters) { + assertSelectQueryFailure( + hql, + resultType, + null, + expectedExceptionType, + expectedExceptionMessage, + expectedExceptionMessageParameters); + } + void assertActualCommand(BsonDocument expectedCommand) { var capturedCommands = testCommandListener.getStartedCommands(); diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java similarity index 86% rename from src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java rename to src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java index b7d71fc7..9c86e330 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -26,12 +26,13 @@ import java.util.List; import org.hibernate.testing.orm.junit.DomainModel; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @DomainModel(annotatedClasses = Book.class) -class SortingIntegrationTests extends AbstractSelectionQueryIntegrationTests { +class SortingSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { private static final List testingBooks = List.of( new Book(1, "War and Peace", 1869, true), @@ -145,7 +146,6 @@ void testSortFieldNotFieldPathExpression() { assertSelectQueryFailure( "from Book ORDER BY length(title)", Book.class, - null, FeatureNotSupportedException.class, "%s does not support sort key not of field path type", MONGO_DBMS_NAME); @@ -156,9 +156,32 @@ void testNullPrecedenceFeatureNotSupported() { assertSelectQueryFailure( "from Book ORDER BY publishYear NULLS LAST", Book.class, - null, FeatureNotSupportedException.class, "%s does not support nulls precedence: NULLS LAST", MONGO_DBMS_NAME); } + + @Nested + class SqlTupleTests { + + @Test + void testOrderBySimpleTuple() { + assertSelectionQuery( + "from Book ORDER BY (publishYear, title) ASC", + Book.class, + null, + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': 1, 'title': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", + getBooksByIds(2, 1, 3, 4, 5)); + } + + @Test + void testOrderByNestedTuple() { + assertSelectionQuery( + "from Book ORDER BY (title, (id, publishYear)) DESC", + Book.class, + null, + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': -1, '_id': -1, 'publishYear': -1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", + getBooksByIds(5, 1, 4, 2, 3)); + } + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index b7653c48..da0ea7ff 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -24,10 +24,11 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATH; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATHS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELD; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.DESC; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; @@ -40,6 +41,7 @@ import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.NOR; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.OR; import static java.lang.String.format; +import static java.util.Collections.singletonList; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.extension.service.StandardServiceRegistryScopedState; @@ -378,14 +380,10 @@ public void visitQuerySpec(QuerySpec querySpec) { var stages = new ArrayList(3); // $match stage - var whereClauseRestrictions = querySpec.getWhereClauseRestrictions(); - if (whereClauseRestrictions != null && !whereClauseRestrictions.isEmpty()) { - var filter = acceptAndYield(whereClauseRestrictions, FILTER); - stages.add(new AstMatchStage(filter)); - } + getAstMatchStage(querySpec).ifPresent(stages::add); // $sort stage - getOptionalAstSortStage(querySpec).ifPresent(stages::add); + getAstSortStage(querySpec).ifPresent(stages::add); // $project stage var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS); @@ -394,26 +392,22 @@ public void visitQuerySpec(QuerySpec querySpec) { astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); } - private Optional getOptionalAstSortStage(QuerySpec querySpec) { + private Optional getAstMatchStage(QuerySpec querySpec) { + var whereClauseRestrictions = querySpec.getWhereClauseRestrictions(); + if (whereClauseRestrictions != null && !whereClauseRestrictions.isEmpty()) { + var filter = acceptAndYield(whereClauseRestrictions, FILTER); + return Optional.of(new AstMatchStage(filter)); + } else { + return Optional.empty(); + } + } + + private Optional getAstSortStage(QuerySpec querySpec) { if (querySpec.hasSortSpecifications()) { var sortFields = new ArrayList( querySpec.getSortSpecifications().size()); - for (SortSpecification sortSpecification : querySpec.getSortSpecifications()) { - if (!isFieldPathExpression(sortSpecification.getSortExpression())) { - throw new FeatureNotSupportedException( - format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); - } - if (sortSpecification.getNullPrecedence() != null - && sortSpecification.getNullPrecedence() != NullPrecedence.NONE) { - throw new FeatureNotSupportedException(format( - "%s does not support nulls precedence: NULLS %s", - MONGO_DBMS_NAME, sortSpecification.getNullPrecedence())); - } - if (sortSpecification.isIgnoreCase()) { - throw new FeatureNotSupportedException( - format("%s does not support case-insensitive sort key", MONGO_DBMS_NAME)); - } - sortFields.add(acceptAndYield(sortSpecification, SORT_FIELD)); + for (var sortSpecification : querySpec.getSortSpecifications()) { + sortFields.addAll(acceptAndYield(sortSpecification, SORT_FIELDS)); } return Optional.of(new AstSortStage(sortFields)); } @@ -541,12 +535,57 @@ public void visitSqlSelectionExpression(SqlSelectionExpression sqlSelectionExpre sqlSelectionExpression.getSelection().getExpression().accept(this); } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // ORDER BY clause + @Override public void visitSortSpecification(SortSpecification sortSpecification) { - var sortFieldPath = acceptAndYield(sortSpecification.getSortExpression(), FIELD_PATH); - var astSortField = new AstSortField( - sortFieldPath, sortSpecification.getSortOrder() == SortDirection.ASCENDING ? ASC : DESC); - astVisitorValueHolder.yield(SORT_FIELD, astSortField); + if (sortSpecification.getNullPrecedence() != null + && sortSpecification.getNullPrecedence() != NullPrecedence.NONE) { + throw new FeatureNotSupportedException(format( + "%s does not support nulls precedence: NULLS %s", + MONGO_DBMS_NAME, sortSpecification.getNullPrecedence())); + } + if (sortSpecification.isIgnoreCase()) { + throw new FeatureNotSupportedException( + format("%s does not support case-insensitive sort key [%s]", MONGO_DBMS_NAME, sortSpecification)); + } + var sortExpression = sortSpecification.getSortExpression(); + final List fieldPaths; + if (sortExpression instanceof SqlTuple sqlTuple) { + fieldPaths = acceptAndYield(sqlTuple, FIELD_PATHS); + } else { + if (!isFieldPathExpression(sortExpression)) { + throw new FeatureNotSupportedException( + format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); + } + var sortFieldPath = acceptAndYield(sortExpression, FIELD_PATH); + fieldPaths = singletonList(sortFieldPath); + } + + var astSortFields = new ArrayList(fieldPaths.size()); + for (var fieldPath : fieldPaths) { + var astSortField = new AstSortField( + fieldPath, sortSpecification.getSortOrder() == SortDirection.ASCENDING ? ASC : DESC); + astSortFields.add(astSortField); + } + astVisitorValueHolder.yield(SORT_FIELDS, astSortFields); + } + + @Override + public void visitTuple(SqlTuple sqlTuple) { + List fieldPaths = new ArrayList<>(sqlTuple.getExpressions().size()); + for (var expression : sqlTuple.getExpressions()) { + if (expression instanceof SqlTuple) { + fieldPaths.addAll(acceptAndYield(expression, FIELD_PATHS)); + } else { + if (!isFieldPathExpression(expression)) { + throw new FeatureNotSupportedException(); + } + fieldPaths.add(acceptAndYield(expression, FIELD_PATH)); + } + } + astVisitorValueHolder.yield(FIELD_PATHS, fieldPaths); } @Override @@ -709,11 +748,6 @@ public void visitEmbeddableTypeLiteral(EmbeddableTypeLiteral embeddableTypeLiter throw new FeatureNotSupportedException(); } - @Override - public void visitTuple(SqlTuple sqlTuple) { - throw new FeatureNotSupportedException(); - } - @Override public void visitCollation(Collation collation) { throw new FeatureNotSupportedException(); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index a6196ae9..561fe9bd 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -45,7 +45,9 @@ final class AstVisitorValueDescriptor { new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor FILTER = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor SORT_FIELD = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor> SORT_FIELDS = new AstVisitorValueDescriptor<>(); + + static final AstVisitorValueDescriptor> FIELD_PATHS = new AstVisitorValueDescriptor<>(); private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; From 526035496e41c35962cc658d0048ec53764c00e0 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 2 May 2025 13:32:17 -0400 Subject: [PATCH 10/50] rename method names to `createMatchStage` and `createSortStage` --- .../internal/translate/AbstractMqlTranslator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index da0ea7ff..a4197102 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -380,10 +380,10 @@ public void visitQuerySpec(QuerySpec querySpec) { var stages = new ArrayList(3); // $match stage - getAstMatchStage(querySpec).ifPresent(stages::add); + createMatchStage(querySpec).ifPresent(stages::add); // $sort stage - getAstSortStage(querySpec).ifPresent(stages::add); + createSortStage(querySpec).ifPresent(stages::add); // $project stage var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS); @@ -392,7 +392,7 @@ public void visitQuerySpec(QuerySpec querySpec) { astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); } - private Optional getAstMatchStage(QuerySpec querySpec) { + private Optional createMatchStage(QuerySpec querySpec) { var whereClauseRestrictions = querySpec.getWhereClauseRestrictions(); if (whereClauseRestrictions != null && !whereClauseRestrictions.isEmpty()) { var filter = acceptAndYield(whereClauseRestrictions, FILTER); @@ -402,7 +402,7 @@ private Optional getAstMatchStage(QuerySpec querySpec) { } } - private Optional getAstSortStage(QuerySpec querySpec) { + private Optional createSortStage(QuerySpec querySpec) { if (querySpec.hasSortSpecifications()) { var sortFields = new ArrayList( querySpec.getSortSpecifications().size()); From 852e86b79711b9d84963f5810625399a63828d79 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 5 May 2025 10:59:39 -0400 Subject: [PATCH 11/50] add unsupported testing case for case-insensitive sort spec --- .../SortingSelectQueryIntegrationTests.java | 18 +++++++++++++++++- .../translate/AbstractMqlTranslator.java | 3 +-- 2 files changed, 18 insertions(+), 3 deletions(-) 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 9c86e330..7b266ab0 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -19,11 +19,14 @@ import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; import java.util.Arrays; import java.util.List; +import org.hibernate.query.NullPrecedence; +import org.hibernate.query.SortDirection; import org.hibernate.testing.orm.junit.DomainModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -142,7 +145,7 @@ void testSortFieldByOrdinalReference() { } @Test - void testSortFieldNotFieldPathExpression() { + void testSortFieldNotFieldPathExpressionNotSupported() { assertSelectQueryFailure( "from Book ORDER BY length(title)", Book.class, @@ -161,6 +164,19 @@ void testNullPrecedenceFeatureNotSupported() { MONGO_DBMS_NAME); } + @Test + void testCaseInsensitiveSortSpecNotSupported() { + sessionFactoryScope.inTransaction(session -> { + var cb = sessionFactoryScope.getSessionFactory().getCriteriaBuilder(); + var criteria = cb.createQuery(Book.class); + var root = criteria.from(Book.class); + criteria.select(root); + criteria.orderBy(cb.sort(root.get("title"), SortDirection.ASCENDING, NullPrecedence.NONE, true)); + assertThatThrownBy(() -> session.createSelectionQuery(criteria).getResultList()) + .isInstanceOf(FeatureNotSupportedException.class); + }); + } + @Nested class SqlTupleTests { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index a4197102..5d82df52 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -547,8 +547,7 @@ public void visitSortSpecification(SortSpecification sortSpecification) { MONGO_DBMS_NAME, sortSpecification.getNullPrecedence())); } if (sortSpecification.isIgnoreCase()) { - throw new FeatureNotSupportedException( - format("%s does not support case-insensitive sort key [%s]", MONGO_DBMS_NAME, sortSpecification)); + throw new FeatureNotSupportedException(); } var sortExpression = sortSpecification.getSortExpression(); final List fieldPaths; From 651e267c5521726918dfa93d468f8a982ffa8294 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 5 May 2025 13:46:41 -0400 Subject: [PATCH 12/50] refactor the tuple related code logic --- .../SortingSelectQueryIntegrationTests.java | 61 +++++++------- .../hibernate/dialect/MongoDialect.java | 5 -- .../translate/AbstractMqlTranslator.java | 82 ++++++++++--------- .../translate/AstVisitorValueDescriptor.java | 3 +- 4 files changed, 76 insertions(+), 75 deletions(-) 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 7b266ab0..8a5e8f78 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -144,37 +144,40 @@ void testSortFieldByOrdinalReference() { new Object[] {"War and Peace", 1869})); } - @Test - void testSortFieldNotFieldPathExpressionNotSupported() { - assertSelectQueryFailure( - "from Book ORDER BY length(title)", - Book.class, - FeatureNotSupportedException.class, - "%s does not support sort key not of field path type", - MONGO_DBMS_NAME); - } + @Nested + class UnsupportedTests { + @Test + void testSortFieldNotFieldPathExpressionNotSupported() { + assertSelectQueryFailure( + "from Book ORDER BY length(title)", + Book.class, + FeatureNotSupportedException.class, + "%s does not support sort key not of field path type", + MONGO_DBMS_NAME); + } - @Test - void testNullPrecedenceFeatureNotSupported() { - assertSelectQueryFailure( - "from Book ORDER BY publishYear NULLS LAST", - Book.class, - FeatureNotSupportedException.class, - "%s does not support nulls precedence: NULLS LAST", - MONGO_DBMS_NAME); - } + @Test + void testNullPrecedenceFeatureNotSupported() { + assertSelectQueryFailure( + "from Book ORDER BY publishYear NULLS LAST", + Book.class, + FeatureNotSupportedException.class, + "%s does not support nulls precedence: NULLS LAST", + MONGO_DBMS_NAME); + } - @Test - void testCaseInsensitiveSortSpecNotSupported() { - sessionFactoryScope.inTransaction(session -> { - var cb = sessionFactoryScope.getSessionFactory().getCriteriaBuilder(); - var criteria = cb.createQuery(Book.class); - var root = criteria.from(Book.class); - criteria.select(root); - criteria.orderBy(cb.sort(root.get("title"), SortDirection.ASCENDING, NullPrecedence.NONE, true)); - assertThatThrownBy(() -> session.createSelectionQuery(criteria).getResultList()) - .isInstanceOf(FeatureNotSupportedException.class); - }); + @Test + void testCaseInsensitiveSortSpecNotSupported() { + sessionFactoryScope.inTransaction(session -> { + var cb = sessionFactoryScope.getSessionFactory().getCriteriaBuilder(); + var criteria = cb.createQuery(Book.class); + var root = criteria.from(Book.class); + criteria.select(root); + criteria.orderBy(cb.sort(root.get("title"), SortDirection.ASCENDING, NullPrecedence.NONE, true)); + assertThatThrownBy(() -> session.createSelectionQuery(criteria).getResultList()) + .isInstanceOf(FeatureNotSupportedException.class); + }); + } } @Nested diff --git a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java index cddc3a60..d3015f9b 100644 --- a/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java +++ b/src/main/java/com/mongodb/hibernate/dialect/MongoDialect.java @@ -97,9 +97,4 @@ public void contribute(TypeContributions typeContributions, ServiceRegistry serv public @Nullable String toQuotedIdentifier(@Nullable String name) { return name; } - - @Override - public boolean supportsNullPrecedence() { - return false; - } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 5d82df52..f67a1cd1 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -24,11 +24,11 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATH; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATHS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.TUPLE; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.DESC; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; @@ -41,7 +41,6 @@ import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.NOR; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.OR; import static java.lang.String.format; -import static java.util.Collections.singletonList; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.extension.service.StandardServiceRegistryScopedState; @@ -61,6 +60,7 @@ import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageIncludeSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstStage; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation; @@ -93,7 +93,6 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.query.NullPrecedence; -import org.hibernate.query.SortDirection; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; @@ -134,6 +133,7 @@ import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; import org.hibernate.sql.ast.tree.expression.SqlSelectionExpression; import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Star; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.expression.TrimSpecification; @@ -241,17 +241,6 @@ List getParameterBinders() { return parameterBinders; } - static String renderMongoAstNode(AstNode rootAstNode) { - try (var stringWriter = new StringWriter(); - var jsonWriter = new JsonWriter(stringWriter, JSON_WRITER_SETTINGS)) { - rootAstNode.render(jsonWriter); - jsonWriter.flush(); - return stringWriter.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - @SuppressWarnings("overloads") R acceptAndYield(Statement statement, AstVisitorValueDescriptor resultDescriptor) { return astVisitorValueHolder.execute(resultDescriptor, () -> statement.accept(this)); @@ -379,13 +368,10 @@ public void visitQuerySpec(QuerySpec querySpec) { var stages = new ArrayList(3); - // $match stage createMatchStage(querySpec).ifPresent(stages::add); - // $sort stage createSortStage(querySpec).ifPresent(stages::add); - // $project stage var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS); stages.add(new AstProjectStage(projectStageSpecifications)); @@ -535,7 +521,7 @@ public void visitSqlSelectionExpression(SqlSelectionExpression sqlSelectionExpre sqlSelectionExpression.getSelection().getExpression().accept(this); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ORDER BY clause @Override @@ -549,42 +535,47 @@ public void visitSortSpecification(SortSpecification sortSpecification) { if (sortSpecification.isIgnoreCase()) { throw new FeatureNotSupportedException(); } + + var astSortOrder = + switch (sortSpecification.getSortOrder()) { + case ASCENDING -> ASC; + case DESCENDING -> DESC; + }; var sortExpression = sortSpecification.getSortExpression(); - final List fieldPaths; - if (sortExpression instanceof SqlTuple sqlTuple) { - fieldPaths = acceptAndYield(sqlTuple, FIELD_PATHS); + var sqlTuple = SqlTupleContainer.getSqlTuple(sortExpression); + if (sqlTuple == null) { + var astSortField = createAstSortField(sortExpression, astSortOrder); + astVisitorValueHolder.yield(SORT_FIELDS, List.of(astSortField)); } else { - if (!isFieldPathExpression(sortExpression)) { - throw new FeatureNotSupportedException( - format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); + var expressions = acceptAndYield(sqlTuple, TUPLE); + var astSortFields = new ArrayList(expressions.size()); + for (var expression : expressions) { + astSortFields.add(createAstSortField(expression, astSortOrder)); } - var sortFieldPath = acceptAndYield(sortExpression, FIELD_PATH); - fieldPaths = singletonList(sortFieldPath); + astVisitorValueHolder.yield(SORT_FIELDS, astSortFields); } + } - var astSortFields = new ArrayList(fieldPaths.size()); - for (var fieldPath : fieldPaths) { - var astSortField = new AstSortField( - fieldPath, sortSpecification.getSortOrder() == SortDirection.ASCENDING ? ASC : DESC); - astSortFields.add(astSortField); + private AstSortField createAstSortField(Expression sortExpression, AstSortOrder astSortOrder) { + if (!isFieldPathExpression(sortExpression)) { + throw new FeatureNotSupportedException( + format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); } - astVisitorValueHolder.yield(SORT_FIELDS, astSortFields); + var fieldPath = acceptAndYield(sortExpression, FIELD_PATH); + return new AstSortField(fieldPath, astSortOrder); } @Override public void visitTuple(SqlTuple sqlTuple) { - List fieldPaths = new ArrayList<>(sqlTuple.getExpressions().size()); + var expressions = new ArrayList(sqlTuple.getExpressions().size()); for (var expression : sqlTuple.getExpressions()) { - if (expression instanceof SqlTuple) { - fieldPaths.addAll(acceptAndYield(expression, FIELD_PATHS)); + if (SqlTupleContainer.getSqlTuple(expression) != null) { + expressions.addAll(acceptAndYield(expression, TUPLE)); } else { - if (!isFieldPathExpression(expression)) { - throw new FeatureNotSupportedException(); - } - fieldPaths.add(acceptAndYield(expression, FIELD_PATH)); + expressions.add(expression); } } - astVisitorValueHolder.yield(FIELD_PATHS, fieldPaths); + astVisitorValueHolder.yield(TUPLE, expressions); } @Override @@ -867,6 +858,17 @@ public void visitCustomTableUpdate(TableUpdateCustomSql tableUpdateCustomSql) { throw new FeatureNotSupportedException(); } + static String renderMongoAstNode(AstNode rootAstNode) { + try (var stringWriter = new StringWriter(); + var jsonWriter = new JsonWriter(stringWriter, JSON_WRITER_SETTINGS)) { + rootAstNode.render(jsonWriter); + jsonWriter.flush(); + return stringWriter.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + static void checkJdbcParameterBindingsSupportability(@Nullable JdbcParameterBindings jdbcParameterBindings) { if (jdbcParameterBindings != null) { for (var jdbcParameterBinding : jdbcParameterBindings.getBindings()) { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index 561fe9bd..a0f8cc99 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -29,6 +29,7 @@ import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import org.hibernate.sql.ast.tree.expression.Expression; @SuppressWarnings("UnusedTypeParameter") final class AstVisitorValueDescriptor { @@ -47,7 +48,7 @@ final class AstVisitorValueDescriptor { static final AstVisitorValueDescriptor> SORT_FIELDS = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor> FIELD_PATHS = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor> TUPLE = new AstVisitorValueDescriptor<>(); private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; From 6d9d765116e36bbbd0395e4dd8e8687787e4e5cb Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 5 May 2025 15:29:48 -0400 Subject: [PATCH 13/50] add default nullness precedence (from SF) handling logic and testing case --- .../SortingSelectQueryIntegrationTests.java | 25 ++++++++++++++++--- .../translate/AbstractMqlTranslator.java | 12 +++++---- 2 files changed, 28 insertions(+), 9 deletions(-) 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 8a5e8f78..f5d688bc 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -20,6 +20,7 @@ import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; 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 com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; @@ -28,6 +29,8 @@ import org.hibernate.query.NullPrecedence; import org.hibernate.query.SortDirection; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +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; @@ -144,6 +147,21 @@ void testSortFieldByOrdinalReference() { new Object[] {"War and Peace", 1869})); } + @Nested + @DomainModel(annotatedClasses = Book.class) + @ServiceRegistry(settings = @Setting(name = DEFAULT_NULL_ORDERING, value = "first")) + class DefaultNullPrecedenceTests extends AbstractSelectionQueryIntegrationTests { + @Test + void testDefaultNullPrecedenceFeatureNotSupported() { + assertSelectQueryFailure( + "from Book ORDER BY publishYear", + Book.class, + FeatureNotSupportedException.class, + "%s does not support null precedence: NULLS FIRST", + MONGO_DBMS_NAME); + } + } + @Nested class UnsupportedTests { @Test @@ -157,12 +175,12 @@ void testSortFieldNotFieldPathExpressionNotSupported() { } @Test - void testNullPrecedenceFeatureNotSupported() { + void testQueryNullPrecedenceFeatureNotSupported() { assertSelectQueryFailure( "from Book ORDER BY publishYear NULLS LAST", Book.class, FeatureNotSupportedException.class, - "%s does not support nulls precedence: NULLS LAST", + "%s does not support null precedence: NULLS LAST", MONGO_DBMS_NAME); } @@ -181,8 +199,7 @@ void testCaseInsensitiveSortSpecNotSupported() { } @Nested - class SqlTupleTests { - + class SortKeyTupleTests { @Test void testOrderBySimpleTuple() { assertSelectionQuery( diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index f67a1cd1..2da9351c 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -526,11 +526,13 @@ public void visitSqlSelectionExpression(SqlSelectionExpression sqlSelectionExpre @Override public void visitSortSpecification(SortSpecification sortSpecification) { - if (sortSpecification.getNullPrecedence() != null - && sortSpecification.getNullPrecedence() != NullPrecedence.NONE) { - throw new FeatureNotSupportedException(format( - "%s does not support nulls precedence: NULLS %s", - MONGO_DBMS_NAME, sortSpecification.getNullPrecedence())); + var nullPrecedence = sortSpecification.getNullPrecedence(); + if (nullPrecedence == null || nullPrecedence == NullPrecedence.NONE) { + nullPrecedence = sessionFactory.getSessionFactoryOptions().getDefaultNullPrecedence(); + } + if (nullPrecedence != null && nullPrecedence != NullPrecedence.NONE) { + throw new FeatureNotSupportedException( + format("%s does not support null precedence: NULLS %s", MONGO_DBMS_NAME, nullPrecedence)); } if (sortSpecification.isIgnoreCase()) { throw new FeatureNotSupportedException(); From abb26e4be01f81a58c4f27ae84b38a8554906d53 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 5 May 2025 18:22:56 -0400 Subject: [PATCH 14/50] resolve conflict with latest main --- ...eanExpressionWhereClauseIntegrationTests.java | 2 ++ .../SortingSelectQueryIntegrationTests.java | 16 ++++------------ .../translate/AbstractMqlTranslator.java | 4 ++-- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java index 2bdb7d5e..3eb51677 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java @@ -33,9 +33,11 @@ class BooleanExpressionWhereClauseIntegrationTests extends AbstractSelectionQuer @BeforeEach void beforeEach() { bookOutOfStock = new Book(); + bookOutOfStock.id = 1; bookOutOfStock.outOfStock = true; bookInStock = new Book(); + bookInStock.id = 2; bookInStock.outOfStock = false; getSessionFactoryScope().inTransaction(session -> { 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 f5d688bc..58c75c7a 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -58,8 +58,8 @@ private static List getBooksByIds(int... ids) { @BeforeEach void beforeEach() { - sessionFactoryScope.inTransaction(session -> testingBooks.forEach(session::persist)); - testCommandListener.clear(); + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); } @ParameterizedTest @@ -68,7 +68,6 @@ void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { assertSelectionQuery( "from Book as b ORDER BY b.publishYear " + sortOrder, Book.class, - null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + (sortOrder == ASC ? "1" : "-1") + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", sortOrder == ASC ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); @@ -80,7 +79,6 @@ void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { assertSelectionQuery( "from Book as b ORDER BY b.title " + sortOrder, Book.class, - null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + (sortOrder == ASC ? "1" : "-1") + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", sortOrder == ASC @@ -99,7 +97,6 @@ void testOrderByMultipleFieldsWithoutTies() { assertSelectionQuery( "from Book where outOfStock = false ORDER BY title ASC, publishYear DESC, id ASC", Book.class, - null, "{ 'aggregate': 'books', 'pipeline': [ {'$match': {'outOfStock': {'$eq': false}}}, { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", getBooksByIds(3, 2, 4, 5)); } @@ -109,7 +106,6 @@ void testOrderByMultipleFieldsWithTies() { assertSelectionQuery( "from Book ORDER BY title ASC, publishYear DESC, id ASC", Book.class, - null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': 1, 'publishYear': -1, '_id': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", resultList -> assertThat(resultList) .satisfiesAnyOf( @@ -122,7 +118,6 @@ void testSortFieldByAlias() { assertSelectionQuery( "select b.title as title, b.publishYear as year from Book as b ORDER BY year DESC, title ASC", Object[].class, - null, "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", List.of( new Object[] {"War and Peace", 2025}, @@ -137,7 +132,6 @@ void testSortFieldByOrdinalReference() { assertSelectionQuery( "select b.title as title, b.publishYear as year from Book as b ORDER BY 1 ASC, 2 DESC", Object[].class, - null, "{'aggregate': 'books', 'pipeline': [{'$sort': {'publishYear': -1, 'title': 1}}, {'$project': {'title': true, 'publishYear': true}}]}", List.of( new Object[] {"Anna Karenina", 1877}, @@ -186,8 +180,8 @@ void testQueryNullPrecedenceFeatureNotSupported() { @Test void testCaseInsensitiveSortSpecNotSupported() { - sessionFactoryScope.inTransaction(session -> { - var cb = sessionFactoryScope.getSessionFactory().getCriteriaBuilder(); + getSessionFactoryScope().inTransaction(session -> { + var cb = getSessionFactoryScope().getSessionFactory().getCriteriaBuilder(); var criteria = cb.createQuery(Book.class); var root = criteria.from(Book.class); criteria.select(root); @@ -205,7 +199,6 @@ void testOrderBySimpleTuple() { assertSelectionQuery( "from Book ORDER BY (publishYear, title) ASC", Book.class, - null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': 1, 'title': 1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", getBooksByIds(2, 1, 3, 4, 5)); } @@ -215,7 +208,6 @@ void testOrderByNestedTuple() { assertSelectionQuery( "from Book ORDER BY (title, (id, publishYear)) DESC", Book.class, - null, "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': -1, '_id': -1, 'publishYear': -1 } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", getBooksByIds(5, 1, 4, 2, 3)); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index ba44149e..4d0a8612 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -29,10 +29,10 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.TUPLE; -import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; -import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.DESC; import static com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue.FALSE; import static com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue.TRUE; +import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; +import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.DESC; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.GT; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.GTE; From 280ded39bac240c21d0ed80e6c5c66f61e6cfa3c Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 6 May 2025 10:17:18 -0400 Subject: [PATCH 15/50] Update src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java Co-authored-by: Viacheslav Babanin --- .../query/select/SortingSelectQueryIntegrationTests.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 58c75c7a..32acd30d 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -168,13 +168,14 @@ void testSortFieldNotFieldPathExpressionNotSupported() { MONGO_DBMS_NAME); } - @Test - void testQueryNullPrecedenceFeatureNotSupported() { + @ParameterizedTest + @ValueSource(strings = {"FIRST", "LAST"}) + void testQueryNullPrecedenceFeatureNotSupported(String nullPrecedence) { assertSelectQueryFailure( - "from Book ORDER BY publishYear NULLS LAST", + "from Book ORDER BY publishYear NULLS " + nullPrecedence, Book.class, FeatureNotSupportedException.class, - "%s does not support null precedence: NULLS LAST", + "%s does not support null precedence: NULLS " + nullPrecedence, MONGO_DBMS_NAME); } From ea0da7679f68c339df85455468567f499c3b6ee9 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 6 May 2025 10:17:41 -0400 Subject: [PATCH 16/50] Update src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java Co-authored-by: Viacheslav Babanin --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 4d0a8612..e6ee23f1 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -549,7 +549,7 @@ public void visitSortSpecification(SortSpecification sortSpecification) { format("%s does not support null precedence: NULLS %s", MONGO_DBMS_NAME, nullPrecedence)); } if (sortSpecification.isIgnoreCase()) { - throw new FeatureNotSupportedException(); + throw new FeatureNotSupportedException("Case-insensitive sorting not supported"); } var astSortOrder = From c188203db6e53c980d6a3224b572e0611443889a Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 6 May 2025 10:23:59 -0400 Subject: [PATCH 17/50] code changes per code review comments --- .../select/SortingSelectQueryIntegrationTests.java | 4 +++- .../internal/translate/AbstractMqlTranslator.java | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) 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 32acd30d..24c5d8c1 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) class SortingSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { @@ -188,7 +189,8 @@ void testCaseInsensitiveSortSpecNotSupported() { criteria.select(root); criteria.orderBy(cb.sort(root.get("title"), SortDirection.ASCENDING, NullPrecedence.NONE, true)); assertThatThrownBy(() -> session.createSelectionQuery(criteria).getResultList()) - .isInstanceOf(FeatureNotSupportedException.class); + .isInstanceOf(FeatureNotSupportedException.class) + .hasMessage("Case-insensitive sorting not supported"); }); } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index e6ee23f1..4dfe0400 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -371,11 +371,8 @@ public void visitQuerySpec(QuerySpec querySpec) { var stages = new ArrayList(3); createMatchStage(querySpec).ifPresent(stages::add); - createSortStage(querySpec).ifPresent(stages::add); - - var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS); - stages.add(new AstProjectStage(projectStageSpecifications)); + stages.add(createProjectStage(querySpec.getSelectClause())); astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); } @@ -402,6 +399,11 @@ private Optional createSortStage(QuerySpec querySpec) { return Optional.empty(); } + private AstProjectStage createProjectStage(SelectClause selectClause) { + var projectStageSpecifications = acceptAndYield(selectClause, PROJECT_STAGE_SPECIFICATIONS); + return new AstProjectStage(projectStageSpecifications); + } + @Override public void visitFromClause(FromClause fromClause) { if (fromClause.getRoots().size() != 1) { From 75412fa1481bb6cba4780b898f6b4116d3f3cee5 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 6 May 2025 10:23:59 -0400 Subject: [PATCH 18/50] code changes per code review comments --- .../SortingSelectQueryIntegrationTests.java | 8 +++++--- .../internal/translate/AbstractMqlTranslator.java | 15 ++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) 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 32acd30d..4ca730d9 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) class SortingSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { @@ -164,8 +165,8 @@ void testSortFieldNotFieldPathExpressionNotSupported() { "from Book ORDER BY length(title)", Book.class, FeatureNotSupportedException.class, - "%s does not support sort key not of field path type", - MONGO_DBMS_NAME); + "TODO-HIBERNATE-%1$d https://jira.mongodb.org/browse/HIBERNATE-%1$d", + 79); } @ParameterizedTest @@ -188,7 +189,8 @@ void testCaseInsensitiveSortSpecNotSupported() { criteria.select(root); criteria.orderBy(cb.sort(root.get("title"), SortDirection.ASCENDING, NullPrecedence.NONE, true)); assertThatThrownBy(() -> session.createSelectionQuery(criteria).getResultList()) - .isInstanceOf(FeatureNotSupportedException.class); + .isInstanceOf(FeatureNotSupportedException.class) + .hasMessage("TODO-HIBERNATE-%1$d https://jira.mongodb.org/browse/HIBERNATE-%1$d", 79); }); } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index e6ee23f1..da36e438 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -371,11 +371,8 @@ public void visitQuerySpec(QuerySpec querySpec) { var stages = new ArrayList(3); createMatchStage(querySpec).ifPresent(stages::add); - createSortStage(querySpec).ifPresent(stages::add); - - var projectStageSpecifications = acceptAndYield(querySpec.getSelectClause(), PROJECT_STAGE_SPECIFICATIONS); - stages.add(new AstProjectStage(projectStageSpecifications)); + stages.add(createProjectStage(querySpec.getSelectClause())); astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); } @@ -402,6 +399,11 @@ private Optional createSortStage(QuerySpec querySpec) { return Optional.empty(); } + private AstProjectStage createProjectStage(SelectClause selectClause) { + var projectStageSpecifications = acceptAndYield(selectClause, PROJECT_STAGE_SPECIFICATIONS); + return new AstProjectStage(projectStageSpecifications); + } + @Override public void visitFromClause(FromClause fromClause) { if (fromClause.getRoots().size() != 1) { @@ -549,7 +551,7 @@ public void visitSortSpecification(SortSpecification sortSpecification) { format("%s does not support null precedence: NULLS %s", MONGO_DBMS_NAME, nullPrecedence)); } if (sortSpecification.isIgnoreCase()) { - throw new FeatureNotSupportedException("Case-insensitive sorting not supported"); + throw new FeatureNotSupportedException("TODO-HIBERNATE-79 https://jira.mongodb.org/browse/HIBERNATE-79"); } var astSortOrder = @@ -574,8 +576,7 @@ public void visitSortSpecification(SortSpecification sortSpecification) { private AstSortField createAstSortField(Expression sortExpression, AstSortOrder astSortOrder) { if (!isFieldPathExpression(sortExpression)) { - throw new FeatureNotSupportedException( - format("%s does not support sort key not of field path type", MONGO_DBMS_NAME)); + throw new FeatureNotSupportedException("TODO-HIBERNATE-79 https://jira.mongodb.org/browse/HIBERNATE-79"); } var fieldPath = acceptAndYield(sortExpression, FIELD_PATH); return new AstSortField(fieldPath, astSortOrder); From 7573ef2afc3bd71d514a13a529924de69f8cbefa Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 6 May 2025 14:33:04 -0400 Subject: [PATCH 19/50] avoid using internal AstSortOrder as EnumSource in SortingSelectQueryIntegrationTests --- .../SortingSelectQueryIntegrationTests.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) 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 4ca730d9..27e3d325 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -17,13 +17,11 @@ package com.mongodb.hibernate.query.select; import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; -import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; 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 com.mongodb.hibernate.internal.FeatureNotSupportedException; -import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; import java.util.Arrays; import java.util.List; import org.hibernate.query.NullPrecedence; @@ -35,7 +33,6 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) @@ -64,25 +61,27 @@ void beforeEach() { } @ParameterizedTest - @EnumSource(AstSortOrder.class) - void testOrderBySingleFieldWithoutTies(AstSortOrder sortOrder) { + @ValueSource(strings = {"ASC", "DESC"}) + void testOrderBySingleFieldWithoutTies(String sortDirection) { assertSelectionQuery( - "from Book as b ORDER BY b.publishYear " + sortOrder, + "from Book as b ORDER BY b.publishYear " + sortDirection, Book.class, - "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + (sortOrder == ASC ? "1" : "-1") + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'publishYear': " + + (sortDirection.equals("ASC") ? "1" : "-1") + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", - sortOrder == ASC ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); + sortDirection.equals("ASC") ? getBooksByIds(2, 1, 3, 4, 5) : getBooksByIds(5, 4, 3, 1, 2)); } @ParameterizedTest - @EnumSource(AstSortOrder.class) - void testOrderBySingleFieldWithTies(AstSortOrder sortOrder) { + @ValueSource(strings = {"ASC", "DESC"}) + void testOrderBySingleFieldWithTies(String sortDirection) { assertSelectionQuery( - "from Book as b ORDER BY b.title " + sortOrder, + "from Book as b ORDER BY b.title " + sortDirection, Book.class, - "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + (sortOrder == ASC ? "1" : "-1") + "{ 'aggregate': 'books', 'pipeline': [ { '$sort': { 'title': " + + (sortDirection.equals("ASC") ? "1" : "-1") + " } }, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true} } ] }", - sortOrder == ASC + sortDirection.equals("ASC") ? resultList -> assertThat(resultList) .satisfiesAnyOf( list -> assertResultListEquals(getBooksByIds(3, 2, 4, 1, 5), list), From 5e4a6fd8ab6c342f97dd50f2a40860f4396fcb6b Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Thu, 24 Apr 2025 14:58:08 -0400 Subject: [PATCH 20/50] Limit/Offset MQL translation --- .../com/mongodb/hibernate/TestDialect.java | 69 +++ .../mongodb/hibernate/query/select/Book.java | 5 + ...imitOffsetFetchClauseIntegrationTests.java | 513 ++++++++++++++++++ .../translate/AbstractMqlTranslator.java | 118 +++- .../translate/AstVisitorValueDescriptor.java | 3 + .../translate/SelectMqlTranslator.java | 13 +- .../command/aggregate/AstLimitStage.java | 32 ++ .../command/aggregate/AstSkipStage.java | 32 ++ .../command/aggregate/AstLimitStageTests.java | 37 ++ .../command/aggregate/AstSkipStageTests.java | 37 ++ 10 files changed, 844 insertions(+), 15 deletions(-) create mode 100644 src/integrationTest/java/com/mongodb/hibernate/TestDialect.java create mode 100644 src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStage.java create mode 100644 src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStage.java create mode 100644 src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStageTests.java create mode 100644 src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStageTests.java diff --git a/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java b/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java new file mode 100644 index 00000000..fce760d3 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate; + +import com.mongodb.hibernate.dialect.MongoDialect; +import java.util.concurrent.atomic.AtomicInteger; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.model.ast.TableMutation; +import org.hibernate.sql.model.jdbc.JdbcMutationOperation; + +public final class TestDialect extends Dialect { + private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); + private final Dialect delegate; + + public TestDialect(DialectResolutionInfo info) { + super(info); + delegate = new MongoDialect(info); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new SqlAstTranslatorFactory() { + @Override + public SqlAstTranslator buildSelectTranslator( + SessionFactoryImplementor sessionFactory, SelectStatement statement) { + selectTranslatingCounter.incrementAndGet(); + return delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildMutationTranslator( + SessionFactoryImplementor sessionFactory, MutationStatement statement) { + return delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildModelMutationTranslator( + TableMutation mutation, SessionFactoryImplementor sessionFactory) { + return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); + } + }; + } + + public int getSelectTranslatingCounter() { + return selectTranslatingCounter.get(); + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java index a1f1fc15..87dc9205 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java @@ -43,4 +43,9 @@ class Book { this.publishYear = publishYear; this.outOfStock = outOfStock; } + + @Override + public String toString() { + return "Book{" + "id=" + id + '}'; + } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java new file mode 100644 index 00000000..515a6732 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -0,0 +1,513 @@ +/* + * 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.hibernate.internal.MongoAssertions.assertTrue; +import static com.mongodb.hibernate.internal.MongoAssertions.fail; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.mongodb.hibernate.TestDialect; +import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.internal.MongoConstants; +import java.util.Arrays; +import java.util.List; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; + +@DomainModel(annotatedClasses = Book.class) +class LimitOffsetFetchClauseIntegrationTests extends AbstractSelectionQueryIntegrationTests { + + private static final List testingBooks = List.of( + new Book(0, "Nostromo", 1904, true), + new Book(1, "The Age of Innocence", 1920, false), + new Book(2, "Remembrance of Things Past", 1913, true), + new Book(3, "The Magic Mountain", 1924, false), + new Book(4, "A Passage to India", 1924, true), + new Book(5, "Ulysses", 1922, false), + new Book(6, "Mrs. Dalloway", 1925, false), + new Book(7, "The Trial", 1925, true), + new Book(8, "Sons and Lovers", 1913, false), + new Book(9, "The Sound and the Fury", 1929, false)); + + private static List getBooksByIds(int... ids) { + return Arrays.stream(ids) + .mapToObj(id -> testingBooks.stream() + .filter(c -> c.id == id) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("id does not exist: " + id))) + .toList(); + } + + @BeforeEach + void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); + } + + @Nested + class WithoutQueryOptionsLimit { + + @Test + void testHqlLimitClauseOnly() { + assertSelectionQuery( + "from Book order by id LIMIT :limit", + Book.class, + q -> q.setParameter("limit", 5), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$limit": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(5), + getBooksByIds(0, 1, 2, 3, 4)); + } + + @Test + void testHqlOffsetClauseOnly() { + assertSelectionQuery( + "from Book order by id OFFSET :offset", + Book.class, + q -> q.setParameter("offset", 7), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$skip": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(7), + getBooksByIds(7, 8, 9)); + } + + @Test + void testHqlLimitAndOffsetClauses() { + assertSelectionQuery( + "from Book order by id LIMIT :limit OFFSET :offset", + Book.class, + q -> q.setParameter("offset", 3).setParameter("limit", 2), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$skip": %d + }, + { + "$limit": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(3, 2), + getBooksByIds(3, 4)); + } + + @Test + void testHqlFetchClauseOnly() { + assertSelectionQuery( + "from Book order by id FETCH FIRST :limit ROWS ONLY", + Book.class, + q -> q.setParameter("limit", 5), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$limit": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(5), + getBooksByIds(0, 1, 2, 3, 4)); + } + } + + @Nested + class WithQueryOptionsLimit { + + @Nested + class WithoutHqlClauses { + @Test + void testQueryOptionsSetFirstResultOnly() { + assertSelectionQuery( + "from Book order by id", + Book.class, + q -> q.setFirstResult(6), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$skip": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(6), + getBooksByIds(6, 7, 8, 9)); + } + + @Test + void testQueryOptionsSetMaxResultOnly() { + assertSelectionQuery( + "from Book order by id", + Book.class, + q -> q.setMaxResults(3), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$limit": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(3), + getBooksByIds(0, 1, 2)); + } + + @Test + void testQueryOptionsSetFirstResultAndMaxResults() { + assertSelectionQuery( + "from Book order by id", + Book.class, + q -> q.setFirstResult(2).setMaxResults(3), + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + { + "$skip": %d + }, + { + "$limit": %d + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(2, 3), + getBooksByIds(2, 3, 4)); + } + } + + @Nested + class WithHqlClauses { + + @ParameterizedTest + @CsvSource({"true,false", "false,true", "true,true"}) + void testWithDirectConflicts(boolean isFirstResultSet, boolean isMaxResultsSet) { + assertTrue(isFirstResultSet || isMaxResultsSet); + var firstResult = 5; + var maxResults = 3; + final List expectedBooks; + if (!isMaxResultsSet) { + expectedBooks = getBooksByIds(5, 6, 7, 8, 9); // firstResult: 5 + } else if (!isFirstResultSet) { + expectedBooks = getBooksByIds(0, 1, 2); // maxResults: 3 + } else { + expectedBooks = getBooksByIds(5, 6, 7); // firstResult: 5 && maxResults: 3 + } + assertSelectionQuery( + "from Book order by id LIMIT :limit OFFSET :offset", + Book.class, + q -> { + q.setParameter("limit", 10) + .setParameter("offset", 0); // hql clauses will be ignored totally + if (isFirstResultSet) { + q.setFirstResult(firstResult); + } + if (isMaxResultsSet) { + q.setMaxResults(maxResults); + } + }, + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + %s + %s + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted( + (isFirstResultSet ? "{'$skip': " + firstResult + "}" : ""), + (isMaxResultsSet ? " {'$limit': " + maxResults + "}" : "")), + expectedBooks); + } + } + } + + @Nested + class FeatureNotSupportedTests { + + @ParameterizedTest + @EnumSource( + value = FetchClauseType.class, + names = {"ROWS_WITH_TIES", "PERCENT_ONLY", "PERCENT_WITH_TIES"}) + void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { + var hqlSuffix = + switch (fetchClauseType) { + case ROWS_ONLY -> fail("ROWS_ONLY should have been excluded from the test"); + case ROWS_WITH_TIES -> "FETCH FIRST :limit ROWS WITH TIES"; + case PERCENT_ONLY -> "FETCH FIRST :limit PERCENT ROWS ONLY"; + case PERCENT_WITH_TIES -> "FETCH FIRST :limit PERCENT ROWS WITH TIES"; + }; + var hql = "from Book order by id " + hqlSuffix; + assertSelectQueryFailure( + hql, + Book.class, + q -> q.setParameter("limit", 10), + FeatureNotSupportedException.class, + "%s does not support '%s' fetch clause type", + MongoConstants.MONGO_DBMS_NAME, + fetchClauseType); + } + } + + @Nested + @DomainModel(annotatedClasses = Book.class) + @ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.QUERY_PLAN_CACHE_ENABLED, value = "true"), + @Setting(name = AvailableSettings.DIALECT, value = "com.mongodb.hibernate.TestDialect") + }) + class QueryPlanCacheTests extends AbstractSelectionQueryIntegrationTests { + + private static final String HQL = "from Book order by id"; + + private TestDialect testDialect; + + @BeforeEach + void beforeEach() { + testDialect = (TestDialect) getSessionFactoryScope() + .getSessionFactory() + .getJdbcServices() + .getDialect(); + } + + @ParameterizedTest + @CsvSource({"true,false", "false,true", "true,true"}) + void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsSet) { + getSessionFactoryScope().inTransaction(session -> { + var query = session.createSelectionQuery(HQL, Book.class); + setQueryOptionsAndQuery(query, isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null); + var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + + query = session.createSelectionQuery(HQL, Book.class); + setQueryOptionsAndQuery(query, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null); + assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount); + }); + } + + private void setQueryOptionsAndQuery(SelectionQuery query, Integer firstResult, Integer maxResults) { + if (firstResult != null) { + query.setFirstResult(firstResult); + } + if (maxResults != null) { + query.setMaxResults(maxResults); + } + query.getResultList(); + } + + @Test + void testCacheInvalidatedDueToQueryOptionsAdded() { + getSessionFactoryScope().inTransaction(session -> { + session.createSelectionQuery(HQL, Book.class).getResultList(); + var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + + session.createSelectionQuery(HQL, Book.class).setFirstResult(1).getResultList(); + assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + + session.createSelectionQuery(HQL, Book.class) + .setFirstResult(1) + .setMaxResults(5) + .getResultList(); + assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 2); + }); + } + + @Test + void testCacheInvalidatedDueToQueryOptionsRemoved() { + getSessionFactoryScope().inTransaction(session -> { + session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); + var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + + session.createSelectionQuery(HQL, Book.class).getResultList(); + assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + }); + } + + @Test + void testCacheInvalidatedDueToDifferentQueryOptions() { + getSessionFactoryScope().inTransaction(session -> { + session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); + var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + + session.createSelectionQuery(HQL, Book.class).setMaxResults(20).getResultList(); + assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + }); + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index da36e438..e7eac46c 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -27,6 +27,7 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SKIP_LIMIT_STAGES; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.TUPLE; import static com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue.FALSE; @@ -43,6 +44,7 @@ import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.NOR; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.OR; import static java.lang.String.format; +import static org.hibernate.query.sqm.FetchClauseType.ROWS_ONLY; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.extension.service.StandardServiceRegistryScopedState; @@ -57,10 +59,12 @@ import com.mongodb.hibernate.internal.translate.mongoast.command.AstInsertCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.AstUpdateCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstAggregateCommand; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstLimitStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstMatchStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageIncludeSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSkipStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortStage; @@ -74,11 +78,14 @@ import java.io.IOException; import java.io.StringWriter; import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import org.bson.BsonBoolean; import org.bson.BsonDecimal128; import org.bson.BsonDouble; @@ -95,6 +102,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.query.NullPrecedence; +import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; @@ -174,6 +182,8 @@ import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.ast.tree.update.Assignment; import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.internal.AbstractJdbcParameter; +import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -187,6 +197,7 @@ import org.hibernate.sql.model.internal.TableInsertStandard; import org.hibernate.sql.model.internal.TableUpdateCustomSql; import org.hibernate.sql.model.internal.TableUpdateStandard; +import org.hibernate.type.BasicType; import org.jspecify.annotations.Nullable; abstract class AbstractMqlTranslator implements SqlAstTranslator { @@ -201,6 +212,10 @@ abstract class AbstractMqlTranslator implements SqlAstT private final Set affectedTableNames = new HashSet<>(); + private @Nullable Limit limit; + private @Nullable JdbcParameter firstRowParameter; + private @Nullable JdbcParameter maxRowsParameter; + AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; assertNotNull(sessionFactory @@ -243,6 +258,18 @@ List getParameterBinders() { return parameterBinders; } + @Nullable JdbcParameter getFirstRowParameter() { + return firstRowParameter; + } + + @Nullable JdbcParameter getMaxRowsParameter() { + return maxRowsParameter; + } + + void setLimit(@Nullable Limit limit) { + this.limit = limit; + } + @SuppressWarnings("overloads") R acceptAndYield(Statement statement, AstVisitorValueDescriptor resultDescriptor) { return astVisitorValueHolder.execute(resultDescriptor, () -> statement.accept(this)); @@ -362,9 +389,6 @@ public void visitQuerySpec(QuerySpec querySpec) { if (!querySpec.getGroupByClauseExpressions().isEmpty()) { throw new FeatureNotSupportedException("GroupBy not supported"); } - if (querySpec.hasOffsetOrFetchClause()) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-70 https://jira.mongodb.org/browse/HIBERNATE-70"); - } var collection = acceptAndYield(querySpec.getFromClause(), COLLECTION_NAME); @@ -372,6 +396,9 @@ public void visitQuerySpec(QuerySpec querySpec) { createMatchStage(querySpec).ifPresent(stages::add); createSortStage(querySpec).ifPresent(stages::add); + + stages.addAll(createSkipOrLimitStages(querySpec)); + stages.add(createProjectStage(querySpec.getSelectClause())); astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); @@ -399,6 +426,10 @@ private Optional createSortStage(QuerySpec querySpec) { return Optional.empty(); } + private List createSkipOrLimitStages(QuerySpec querySpec) { + return astVisitorValueHolder.execute(SKIP_LIMIT_STAGES, () -> visitOffsetFetchClause(querySpec)); + } + private AstProjectStage createProjectStage(SelectClause selectClause) { var projectStageSpecifications = acceptAndYield(selectClause, PROJECT_STAGE_SPECIFICATIONS); return new AstProjectStage(projectStageSpecifications); @@ -582,6 +613,48 @@ private AstSortField createAstSortField(Expression sortExpression, AstSortOrder return new AstSortField(fieldPath, astSortOrder); } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // LIMIT/OFFSET/FETCH clause + + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + var stages = createSkipAndLimitStages(queryPart); + astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, stages); + } + + private List createSkipAndLimitStages(QueryPart queryPart) { + var stages = new ArrayList(2); + final Expression skipExpression; + final Expression limitExpression; + if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { + var basicType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); + if (limit.getFirstRow() != null) { + firstRowParameter = new LimitJdbcParameter(basicType, Limit::getFirstRow); + } + if (limit.getMaxRows() != null) { + maxRowsParameter = new LimitJdbcParameter(basicType, Limit::getMaxRows); + } + skipExpression = firstRowParameter; + limitExpression = maxRowsParameter; + } else { + if (queryPart.getFetchClauseType() != ROWS_ONLY) { + throw new FeatureNotSupportedException(format( + "%s does not support '%s' fetch clause type", MONGO_DBMS_NAME, queryPart.getFetchClauseType())); + } + skipExpression = queryPart.getOffsetClauseExpression(); + limitExpression = queryPart.getFetchClauseExpression(); + } + if (skipExpression != null) { + var offsetStage = new AstSkipStage(acceptAndYield(skipExpression, FIELD_VALUE)); + stages.add(offsetStage); + } + if (limitExpression != null) { + var limitStage = new AstLimitStage(acceptAndYield(limitExpression, FIELD_VALUE)); + stages.add(limitStage); + } + return stages; + } + @Override public void visitTuple(SqlTuple sqlTuple) { var expressions = new ArrayList(sqlTuple.getExpressions().size()); @@ -620,11 +693,6 @@ public void visitQueryGroup(QueryGroup queryGroup) { throw new FeatureNotSupportedException(); } - @Override - public void visitOffsetFetchClause(QueryPart queryPart) { - throw new FeatureNotSupportedException(); - } - @Override public void visitSqlSelection(SqlSelection sqlSelection) { throw new FeatureNotSupportedException(); @@ -931,12 +999,6 @@ static void checkQueryOptionsSupportability(QueryOptions queryOptions) { && !queryOptions.getDatabaseHints().isEmpty()) { throw new FeatureNotSupportedException("'databaseHints' in QueryOptions not supported"); } - if (queryOptions.getFetchSize() != null) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-54 https://jira.mongodb.org/browse/HIBERNATE-54"); - } - if (queryOptions.getLimit() != null && !queryOptions.getLimit().isEmpty()) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-70 https://jira.mongodb.org/browse/HIBERNATE-70"); - } } private static AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) { @@ -994,4 +1056,32 @@ private static BsonValue toBsonValue(@Nullable Object queryLiteral) { } throw new FeatureNotSupportedException("Unsupported Java type: " + queryLiteral.getClass()); } + + private static final class LimitJdbcParameter extends AbstractJdbcParameter { + + private final Function parameterValueAccess; + + public LimitJdbcParameter(BasicType type, Function parameterValueAccess) { + super(type); + this.parameterValueAccess = parameterValueAccess; + } + + @SuppressWarnings("unchecked") + @Override + public void bindParameterValue( + PreparedStatement statement, + int startPosition, + JdbcParameterBindings jdbcParamBindings, + ExecutionContext executionContext) + throws SQLException { + getJdbcMapping() + .getJdbcValueBinder() + .bind( + statement, + parameterValueAccess.apply( + executionContext.getQueryOptions().getLimit()), + startPosition, + executionContext.getSession()); + } + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index a0f8cc99..7bb80b04 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -23,6 +23,7 @@ import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; +import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstStage; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter; import java.lang.reflect.Modifier; import java.util.Collections; @@ -50,6 +51,8 @@ final class AstVisitorValueDescriptor { static final AstVisitorValueDescriptor> TUPLE = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor> SKIP_LIMIT_STAGES = new AstVisitorValueDescriptor<>(); + private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; static { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 3a745ac2..69bfba4d 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -19,10 +19,12 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; +import java.util.Collections; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcLockStrategy; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; @@ -49,6 +51,9 @@ public JdbcOperationQuerySelect translate( checkJdbcParameterBindingsSupportability(jdbcParameterBindings); checkQueryOptionsSupportability(queryOptions); + setLimit( + queryOptions.getLimit() == null ? null : queryOptions.getLimit().makeCopy()); + var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE); var jdbcValuesMappingProducer = jdbcValuesMappingProducerProvider.buildMappingProducer(selectStatement, getSessionFactory()); @@ -57,6 +62,12 @@ public JdbcOperationQuerySelect translate( renderMongoAstNode(aggregateCommand), getParameterBinders(), jdbcValuesMappingProducer, - getAffectedTableNames()); + getAffectedTableNames(), + 0, + Integer.MAX_VALUE, + Collections.emptyMap(), + JdbcLockStrategy.NONE, + getFirstRowParameter(), + getMaxRowsParameter()); } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStage.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStage.java new file mode 100644 index 00000000..2cc23673 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStage.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import com.mongodb.hibernate.internal.translate.mongoast.AstValue; +import org.bson.BsonWriter; + +public record AstLimitStage(AstValue value) implements AstStage { + @Override + public void render(BsonWriter writer) { + writer.writeStartDocument(); + { + writer.writeName("$limit"); + value.render(writer); + } + writer.writeEndDocument(); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStage.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStage.java new file mode 100644 index 00000000..f6ba9e7b --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStage.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import com.mongodb.hibernate.internal.translate.mongoast.AstValue; +import org.bson.BsonWriter; + +public record AstSkipStage(AstValue value) implements AstStage { + @Override + public void render(BsonWriter writer) { + writer.writeStartDocument(); + { + writer.writeName("$skip"); + value.render(writer); + } + writer.writeEndDocument(); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStageTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStageTests.java new file mode 100644 index 00000000..b3512a1f --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStageTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; + +import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; +import org.bson.BsonInt32; +import org.junit.jupiter.api.Test; + +class AstLimitStageTests { + + @Test + void testRendering() { + var limitValue = 10; + var astLimitStage = new AstLimitStage(new AstLiteralValue(new BsonInt32(limitValue))); + + var expectedJson = """ + {"$limit": %d}\ + """.formatted(limitValue); + assertRender(expectedJson, astLimitStage); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStageTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStageTests.java new file mode 100644 index 00000000..9607bbe8 --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStageTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.command.aggregate; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; + +import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; +import org.bson.BsonInt32; +import org.junit.jupiter.api.Test; + +class AstSkipStageTests { + + @Test + void testRendering() { + var skipValue = 5; + var astSkipStage = new AstSkipStage(new AstLiteralValue(new BsonInt32(skipValue))); + + var expectedJson = """ + {"$skip": %d}\ + """.formatted(skipValue); + assertRender(expectedJson, astSkipStage); + } +} From aa9357d043882ba96542d06b46c8092f59097e39 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 23 May 2025 13:42:07 -0400 Subject: [PATCH 21/50] make TestDialect static inner class in LimitOffsetFetchClauseIntegrationTests --- .../com/mongodb/hibernate/TestDialect.java | 69 ------------------- ...imitOffsetFetchClauseIntegrationTests.java | 57 ++++++++++++++- 2 files changed, 55 insertions(+), 71 deletions(-) delete mode 100644 src/integrationTest/java/com/mongodb/hibernate/TestDialect.java diff --git a/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java b/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java deleted file mode 100644 index fce760d3..00000000 --- a/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2025-present MongoDB, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mongodb.hibernate; - -import com.mongodb.hibernate.dialect.MongoDialect; -import java.util.concurrent.atomic.AtomicInteger; -import org.hibernate.dialect.Dialect; -import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.SqlAstTranslatorFactory; -import org.hibernate.sql.ast.tree.MutationStatement; -import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; -import org.hibernate.sql.model.ast.TableMutation; -import org.hibernate.sql.model.jdbc.JdbcMutationOperation; - -public final class TestDialect extends Dialect { - private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); - private final Dialect delegate; - - public TestDialect(DialectResolutionInfo info) { - super(info); - delegate = new MongoDialect(info); - } - - @Override - public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { - return new SqlAstTranslatorFactory() { - @Override - public SqlAstTranslator buildSelectTranslator( - SessionFactoryImplementor sessionFactory, SelectStatement statement) { - selectTranslatingCounter.incrementAndGet(); - return delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement); - } - - @Override - public SqlAstTranslator buildMutationTranslator( - SessionFactoryImplementor sessionFactory, MutationStatement statement) { - return delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); - } - - @Override - public SqlAstTranslator buildModelMutationTranslator( - TableMutation mutation, SessionFactoryImplementor sessionFactory) { - return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); - } - }; - } - - public int getSelectTranslatingCounter() { - return selectTranslatingCounter.get(); - } -} 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 515a6732..a37b4c28 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -20,14 +20,26 @@ import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.mongodb.hibernate.TestDialect; +import com.mongodb.hibernate.dialect.MongoDialect; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.MongoConstants; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.SelectionQuery; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.model.ast.TableMutation; +import org.hibernate.sql.model.jdbc.JdbcMutationOperation; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.Setting; @@ -431,7 +443,10 @@ void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { @ServiceRegistry( settings = { @Setting(name = AvailableSettings.QUERY_PLAN_CACHE_ENABLED, value = "true"), - @Setting(name = AvailableSettings.DIALECT, value = "com.mongodb.hibernate.TestDialect") + @Setting( + name = AvailableSettings.DIALECT, + value = + "com.mongodb.hibernate.query.select.LimitOffsetFetchClauseIntegrationTests$TestDialect"), }) class QueryPlanCacheTests extends AbstractSelectionQueryIntegrationTests { @@ -510,4 +525,42 @@ void testCacheInvalidatedDueToDifferentQueryOptions() { }); } } + + public static final class TestDialect extends Dialect { + private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); + private final Dialect delegate; + + public TestDialect(DialectResolutionInfo info) { + super(info); + delegate = new MongoDialect(info); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new SqlAstTranslatorFactory() { + @Override + public SqlAstTranslator buildSelectTranslator( + SessionFactoryImplementor sessionFactory, SelectStatement statement) { + selectTranslatingCounter.incrementAndGet(); + return delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildMutationTranslator( + SessionFactoryImplementor sessionFactory, MutationStatement statement) { + return delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildModelMutationTranslator( + TableMutation mutation, SessionFactoryImplementor sessionFactory) { + return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); + } + }; + } + + public int getSelectTranslatingCounter() { + return selectTranslatingCounter.get(); + } + } } From 966d099eb2afd72b61df489b9c6692103832bee5 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 23 May 2025 13:42:07 -0400 Subject: [PATCH 22/50] make TestDialect static inner class in LimitOffsetFetchClauseIntegrationTests --- .../com/mongodb/hibernate/TestDialect.java | 69 ---------------- ...imitOffsetFetchClauseIntegrationTests.java | 79 ++++++++++++++++--- 2 files changed, 66 insertions(+), 82 deletions(-) delete mode 100644 src/integrationTest/java/com/mongodb/hibernate/TestDialect.java diff --git a/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java b/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java deleted file mode 100644 index fce760d3..00000000 --- a/src/integrationTest/java/com/mongodb/hibernate/TestDialect.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2025-present MongoDB, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mongodb.hibernate; - -import com.mongodb.hibernate.dialect.MongoDialect; -import java.util.concurrent.atomic.AtomicInteger; -import org.hibernate.dialect.Dialect; -import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.SqlAstTranslatorFactory; -import org.hibernate.sql.ast.tree.MutationStatement; -import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; -import org.hibernate.sql.model.ast.TableMutation; -import org.hibernate.sql.model.jdbc.JdbcMutationOperation; - -public final class TestDialect extends Dialect { - private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); - private final Dialect delegate; - - public TestDialect(DialectResolutionInfo info) { - super(info); - delegate = new MongoDialect(info); - } - - @Override - public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { - return new SqlAstTranslatorFactory() { - @Override - public SqlAstTranslator buildSelectTranslator( - SessionFactoryImplementor sessionFactory, SelectStatement statement) { - selectTranslatingCounter.incrementAndGet(); - return delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement); - } - - @Override - public SqlAstTranslator buildMutationTranslator( - SessionFactoryImplementor sessionFactory, MutationStatement statement) { - return delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); - } - - @Override - public SqlAstTranslator buildModelMutationTranslator( - TableMutation mutation, SessionFactoryImplementor sessionFactory) { - return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); - } - }; - } - - public int getSelectTranslatingCounter() { - return selectTranslatingCounter.get(); - } -} 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 515a6732..2547785c 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -20,14 +20,26 @@ import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import com.mongodb.hibernate.TestDialect; +import com.mongodb.hibernate.dialect.MongoDialect; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.MongoConstants; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.SelectionQuery; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.model.ast.TableMutation; +import org.hibernate.sql.model.jdbc.JdbcMutationOperation; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.Setting; @@ -431,17 +443,20 @@ void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { @ServiceRegistry( settings = { @Setting(name = AvailableSettings.QUERY_PLAN_CACHE_ENABLED, value = "true"), - @Setting(name = AvailableSettings.DIALECT, value = "com.mongodb.hibernate.TestDialect") + @Setting( + name = AvailableSettings.DIALECT, + value = + "com.mongodb.hibernate.query.select.LimitOffsetFetchClauseIntegrationTests$MqlTranslateCacheTestingDialect"), }) class QueryPlanCacheTests extends AbstractSelectionQueryIntegrationTests { private static final String HQL = "from Book order by id"; - private TestDialect testDialect; + private MqlTranslateCacheTestingDialect mqlTranslateCacheTestingDialect; @BeforeEach void beforeEach() { - testDialect = (TestDialect) getSessionFactoryScope() + mqlTranslateCacheTestingDialect = (MqlTranslateCacheTestingDialect) getSessionFactoryScope() .getSessionFactory() .getJdbcServices() .getDialect(); @@ -453,11 +468,11 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS getSessionFactoryScope().inTransaction(session -> { var query = session.createSelectionQuery(HQL, Book.class); setQueryOptionsAndQuery(query, isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null); - var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); query = session.createSelectionQuery(HQL, Book.class); setQueryOptionsAndQuery(query, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null); - assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount); }); } @@ -475,16 +490,16 @@ private void setQueryOptionsAndQuery(SelectionQuery query, Integer firstRe void testCacheInvalidatedDueToQueryOptionsAdded() { getSessionFactoryScope().inTransaction(session -> { session.createSelectionQuery(HQL, Book.class).getResultList(); - var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); session.createSelectionQuery(HQL, Book.class).setFirstResult(1).getResultList(); - assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); session.createSelectionQuery(HQL, Book.class) .setFirstResult(1) .setMaxResults(5) .getResultList(); - assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 2); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 2); }); } @@ -492,10 +507,10 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { void testCacheInvalidatedDueToQueryOptionsRemoved() { getSessionFactoryScope().inTransaction(session -> { session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); - var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); session.createSelectionQuery(HQL, Book.class).getResultList(); - assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); }); } @@ -503,11 +518,49 @@ void testCacheInvalidatedDueToQueryOptionsRemoved() { void testCacheInvalidatedDueToDifferentQueryOptions() { getSessionFactoryScope().inTransaction(session -> { session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); - var initialSelectTranslatingCount = testDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); session.createSelectionQuery(HQL, Book.class).setMaxResults(20).getResultList(); - assertThat(testDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); }); } } + + public static final class MqlTranslateCacheTestingDialect extends Dialect { + private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); + private final Dialect delegate; + + public MqlTranslateCacheTestingDialect(DialectResolutionInfo info) { + super(info); + delegate = new MongoDialect(info); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new SqlAstTranslatorFactory() { + @Override + public SqlAstTranslator buildSelectTranslator( + SessionFactoryImplementor sessionFactory, SelectStatement statement) { + selectTranslatingCounter.incrementAndGet(); + return delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildMutationTranslator( + SessionFactoryImplementor sessionFactory, MutationStatement statement) { + return delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildModelMutationTranslator( + TableMutation mutation, SessionFactoryImplementor sessionFactory) { + return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); + } + }; + } + + public int getSelectTranslatingCounter() { + return selectTranslatingCounter.get(); + } + } } From 0a3a65939ccd98d956cc35904fe61f99e21cfbbf Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 23 May 2025 14:52:51 -0400 Subject: [PATCH 23/50] improve naming --- ...imitOffsetFetchClauseIntegrationTests.java | 15 +++-- .../translate/AbstractMqlTranslator.java | 58 ++++++++----------- .../translate/SelectMqlTranslator.java | 20 ++++--- 3 files changed, 44 insertions(+), 49 deletions(-) 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 2547785c..a937af4e 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -472,7 +472,8 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS query = session.createSelectionQuery(HQL, Book.class); setQueryOptionsAndQuery(query, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + .isEqualTo(initialSelectTranslatingCount); }); } @@ -493,13 +494,15 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); session.createSelectionQuery(HQL, Book.class).setFirstResult(1).getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + .isEqualTo(initialSelectTranslatingCount + 1); session.createSelectionQuery(HQL, Book.class) .setFirstResult(1) .setMaxResults(5) .getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 2); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + .isEqualTo(initialSelectTranslatingCount + 2); }); } @@ -510,7 +513,8 @@ void testCacheInvalidatedDueToQueryOptionsRemoved() { var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); session.createSelectionQuery(HQL, Book.class).getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + .isEqualTo(initialSelectTranslatingCount + 1); }); } @@ -521,7 +525,8 @@ void testCacheInvalidatedDueToDifferentQueryOptions() { var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); session.createSelectionQuery(HQL, Book.class).setMaxResults(20).getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()).isEqualTo(initialSelectTranslatingCount + 1); + assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + .isEqualTo(initialSelectTranslatingCount + 1); }); } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 9e3529ab..75e02256 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -208,9 +208,9 @@ abstract class AbstractMqlTranslator implements SqlAstT private final Set affectedTableNames = new HashSet<>(); - private @Nullable Limit limit; - private @Nullable JdbcParameter firstRowParameter; - private @Nullable JdbcParameter maxRowsParameter; + @Nullable Limit limit; + @Nullable JdbcParameter firstRowJdbcParameter; + @Nullable JdbcParameter maxRowsJdbcParameter; AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; @@ -254,18 +254,6 @@ List getParameterBinders() { return parameterBinders; } - @Nullable JdbcParameter getFirstRowParameter() { - return firstRowParameter; - } - - @Nullable JdbcParameter getMaxRowsParameter() { - return maxRowsParameter; - } - - void setLimit(@Nullable Limit limit) { - this.limit = limit; - } - @SuppressWarnings("overloads") R acceptAndYield(Statement statement, AstVisitorValueDescriptor resultDescriptor) { return astVisitorValueHolder.execute(resultDescriptor, () -> statement.accept(this)); @@ -383,7 +371,7 @@ public void visitQuerySpec(QuerySpec querySpec) { createMatchStage(querySpec).ifPresent(stages::add); createSortStage(querySpec).ifPresent(stages::add); - stages.addAll(createSkipOrLimitStages(querySpec)); + stages.addAll(createSkipLimitStages(querySpec)); stages.add(createProjectStage(querySpec.getSelectClause())); astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); @@ -411,7 +399,7 @@ private Optional createSortStage(QuerySpec querySpec) { return Optional.empty(); } - private List createSkipOrLimitStages(QuerySpec querySpec) { + private List createSkipLimitStages(QuerySpec querySpec) { return astVisitorValueHolder.execute(SKIP_LIMIT_STAGES, () -> visitOffsetFetchClause(querySpec)); } @@ -597,24 +585,24 @@ private AstSortField createAstSortField(Expression sortExpression, AstSortOrder @Override public void visitOffsetFetchClause(QueryPart queryPart) { - var stages = createSkipAndLimitStages(queryPart); - astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, stages); + var skipLimitStages = createSkipLimitStages(queryPart); + astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, skipLimitStages); } - private List createSkipAndLimitStages(QueryPart queryPart) { - var stages = new ArrayList(2); + private List createSkipLimitStages(QueryPart queryPart) { + var skipLimitStages = new ArrayList(2); final Expression skipExpression; final Expression limitExpression; if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { - var basicType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); + var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); if (limit.getFirstRow() != null) { - firstRowParameter = new LimitJdbcParameter(basicType, Limit::getFirstRow); + firstRowJdbcParameter = new LimitJdbcParameter(basicIntegerType, Limit::getFirstRow); } if (limit.getMaxRows() != null) { - maxRowsParameter = new LimitJdbcParameter(basicType, Limit::getMaxRows); + maxRowsJdbcParameter = new LimitJdbcParameter(basicIntegerType, Limit::getMaxRows); } - skipExpression = firstRowParameter; - limitExpression = maxRowsParameter; + skipExpression = firstRowJdbcParameter; + limitExpression = maxRowsJdbcParameter; } else { if (queryPart.getFetchClauseType() != ROWS_ONLY) { throw new FeatureNotSupportedException(format( @@ -624,14 +612,14 @@ private List createSkipAndLimitStages(QueryPart queryPart) { limitExpression = queryPart.getFetchClauseExpression(); } if (skipExpression != null) { - var offsetStage = new AstSkipStage(acceptAndYield(skipExpression, FIELD_VALUE)); - stages.add(offsetStage); + var skipValue = acceptAndYield(skipExpression, FIELD_VALUE); + skipLimitStages.add(new AstSkipStage(skipValue)); } if (limitExpression != null) { - var limitStage = new AstLimitStage(acceptAndYield(limitExpression, FIELD_VALUE)); - stages.add(limitStage); + var limitValue = acceptAndYield(limitExpression, FIELD_VALUE); + skipLimitStages.add(new AstLimitStage(limitValue)); } - return stages; + return skipLimitStages; } @Override @@ -1038,11 +1026,11 @@ private static BsonValue toBsonValue(@Nullable Object queryLiteral) { private static final class LimitJdbcParameter extends AbstractJdbcParameter { - private final Function parameterValueAccess; + private final Function limitValueAccess; - public LimitJdbcParameter(BasicType type, Function parameterValueAccess) { + public LimitJdbcParameter(BasicType type, Function limitValueAccess) { super(type); - this.parameterValueAccess = parameterValueAccess; + this.limitValueAccess = limitValueAccess; } @SuppressWarnings("unchecked") @@ -1057,7 +1045,7 @@ public void bindParameterValue( .getJdbcValueBinder() .bind( statement, - parameterValueAccess.apply( + limitValueAccess.apply( executionContext.getQueryOptions().getLimit()), startPosition, executionContext.getSession()); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 69bfba4d..9778a1a0 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -17,14 +17,15 @@ package com.mongodb.hibernate.internal.translate; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; +import static java.lang.Integer.MAX_VALUE; +import static java.util.Collections.emptyMap; import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; +import static org.hibernate.sql.exec.spi.JdbcLockStrategy.NONE; -import java.util.Collections; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcLockStrategy; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; @@ -51,8 +52,9 @@ public JdbcOperationQuerySelect translate( checkJdbcParameterBindingsSupportability(jdbcParameterBindings); checkQueryOptionsSupportability(queryOptions); - setLimit( - queryOptions.getLimit() == null ? null : queryOptions.getLimit().makeCopy()); + if (queryOptions.getLimit() != null) { + limit = queryOptions.getLimit().makeCopy(); + } var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE); var jdbcValuesMappingProducer = @@ -64,10 +66,10 @@ public JdbcOperationQuerySelect translate( jdbcValuesMappingProducer, getAffectedTableNames(), 0, - Integer.MAX_VALUE, - Collections.emptyMap(), - JdbcLockStrategy.NONE, - getFirstRowParameter(), - getMaxRowsParameter()); + MAX_VALUE, + emptyMap(), + NONE, + firstRowJdbcParameter, + maxRowsJdbcParameter); } } From 73d457c4818220ee5f4e57a48e7c98e1c4f5786c Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Fri, 23 May 2025 15:48:36 -0400 Subject: [PATCH 24/50] improve LimitOffsetFetchClauseIntegrationTests --- ...bstractSelectionQueryIntegrationTests.java | 7 -- ...imitOffsetFetchClauseIntegrationTests.java | 67 +++++++++++-------- .../translate/AbstractMqlTranslator.java | 2 + 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java index f9560aab..0ddd2aac 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java @@ -143,11 +143,4 @@ void assertActualCommand(BsonDocument expectedCommand) { .asInstanceOf(InstanceOfAssertFactories.MAP) .containsAllEntriesOf(expectedCommand); } - - @SuppressWarnings("unchecked") - static void assertResultListEquals(List expectedResultList, List actualResultList) { - assertThat((List) actualResultList) - .usingRecursiveFieldByFieldElementComparator() - .containsExactlyElementsOf(expectedResultList); - } } 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 a937af4e..5632ad94 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -19,6 +19,9 @@ import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; import static com.mongodb.hibernate.internal.MongoAssertions.fail; 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.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE; import com.mongodb.hibernate.dialect.MongoDialect; import com.mongodb.hibernate.internal.FeatureNotSupportedException; @@ -26,7 +29,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; -import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -379,7 +381,7 @@ void testWithDirectConflicts(boolean isFirstResultSet, boolean isMaxResultsSet) } }, """ - { + { "aggregate": "books", "pipeline": [ { @@ -404,8 +406,8 @@ void testWithDirectConflicts(boolean isFirstResultSet, boolean isMaxResultsSet) } """ .formatted( - (isFirstResultSet ? "{'$skip': " + firstResult + "}" : ""), - (isMaxResultsSet ? " {'$limit': " + maxResults + "}" : "")), + (isFirstResultSet ? "{\"$skip\": " + firstResult + "}," : ""), + (isMaxResultsSet ? "{\"$limit\": " + maxResults + "}," : "")), expectedBooks); } } @@ -415,9 +417,7 @@ void testWithDirectConflicts(boolean isFirstResultSet, boolean isMaxResultsSet) class FeatureNotSupportedTests { @ParameterizedTest - @EnumSource( - value = FetchClauseType.class, - names = {"ROWS_WITH_TIES", "PERCENT_ONLY", "PERCENT_WITH_TIES"}) + @EnumSource(value = FetchClauseType.class, mode = EXCLUDE, names = "ROWS_ONLY") void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { var hqlSuffix = switch (fetchClauseType) { @@ -442,21 +442,21 @@ void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { @DomainModel(annotatedClasses = Book.class) @ServiceRegistry( settings = { - @Setting(name = AvailableSettings.QUERY_PLAN_CACHE_ENABLED, value = "true"), + @Setting(name = QUERY_PLAN_CACHE_ENABLED, value = "true"), @Setting( - name = AvailableSettings.DIALECT, + name = DIALECT, value = - "com.mongodb.hibernate.query.select.LimitOffsetFetchClauseIntegrationTests$MqlTranslateCacheTestingDialect"), + "com.mongodb.hibernate.query.select.LimitOffsetFetchClauseIntegrationTests$TranslatingCacheTestingDialect"), }) class QueryPlanCacheTests extends AbstractSelectionQueryIntegrationTests { private static final String HQL = "from Book order by id"; - private MqlTranslateCacheTestingDialect mqlTranslateCacheTestingDialect; + private TranslatingCacheTestingDialect translatingCacheTestingDialect; @BeforeEach void beforeEach() { - mqlTranslateCacheTestingDialect = (MqlTranslateCacheTestingDialect) getSessionFactoryScope() + translatingCacheTestingDialect = (TranslatingCacheTestingDialect) getSessionFactoryScope() .getSessionFactory() .getJdbcServices() .getDialect(); @@ -466,18 +466,21 @@ void beforeEach() { @CsvSource({"true,false", "false,true", "true,true"}) void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsSet) { getSessionFactoryScope().inTransaction(session -> { - var query = session.createSelectionQuery(HQL, Book.class); - setQueryOptionsAndQuery(query, isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null); - var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); + var firstQuery = session.createSelectionQuery(HQL, Book.class); + setQueryOptionsAndQuery(firstQuery, isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); + + assertThat(initialSelectTranslatingCount).isPositive(); - query = session.createSelectionQuery(HQL, Book.class); - setQueryOptionsAndQuery(query, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + var secondQuery = session.createSelectionQuery(HQL, Book.class); + setQueryOptionsAndQuery(secondQuery, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null); + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount); }); } - private void setQueryOptionsAndQuery(SelectionQuery query, Integer firstResult, Integer maxResults) { + private static void setQueryOptionsAndQuery( + SelectionQuery query, Integer firstResult, Integer maxResults) { if (firstResult != null) { query.setFirstResult(firstResult); } @@ -491,17 +494,19 @@ private void setQueryOptionsAndQuery(SelectionQuery query, Integer firstRe void testCacheInvalidatedDueToQueryOptionsAdded() { getSessionFactoryScope().inTransaction(session -> { session.createSelectionQuery(HQL, Book.class).getResultList(); - var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); + + assertThat(initialSelectTranslatingCount).isPositive(); session.createSelectionQuery(HQL, Book.class).setFirstResult(1).getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); session.createSelectionQuery(HQL, Book.class) .setFirstResult(1) .setMaxResults(5) .getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 2); }); } @@ -510,10 +515,12 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { void testCacheInvalidatedDueToQueryOptionsRemoved() { getSessionFactoryScope().inTransaction(session -> { session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); - var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); + + assertThat(initialSelectTranslatingCount).isPositive(); session.createSelectionQuery(HQL, Book.class).getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); }); } @@ -522,20 +529,22 @@ void testCacheInvalidatedDueToQueryOptionsRemoved() { void testCacheInvalidatedDueToDifferentQueryOptions() { getSessionFactoryScope().inTransaction(session -> { session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); - var initialSelectTranslatingCount = mqlTranslateCacheTestingDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); + + assertThat(initialSelectTranslatingCount).isPositive(); session.createSelectionQuery(HQL, Book.class).setMaxResults(20).getResultList(); - assertThat(mqlTranslateCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); }); } } - public static final class MqlTranslateCacheTestingDialect extends Dialect { + public static final class TranslatingCacheTestingDialect extends Dialect { private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); private final Dialect delegate; - public MqlTranslateCacheTestingDialect(DialectResolutionInfo info) { + public TranslatingCacheTestingDialect(DialectResolutionInfo info) { super(info); delegate = new MongoDialect(info); } @@ -553,7 +562,7 @@ public SqlAstTranslator buildSelectTranslator( @Override public SqlAstTranslator buildMutationTranslator( SessionFactoryImplementor sessionFactory, MutationStatement statement) { - return delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); + throw new IllegalStateException("mutation translator not expected"); } @Override diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 75e02256..f753b097 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -209,7 +209,9 @@ abstract class AbstractMqlTranslator implements SqlAstT private final Set affectedTableNames = new HashSet<>(); @Nullable Limit limit; + @Nullable JdbcParameter firstRowJdbcParameter; + @Nullable JdbcParameter maxRowsJdbcParameter; AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { From b596f0d2a6695f38e3b577c12d3853af544613a6 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 26 May 2025 08:08:30 -0400 Subject: [PATCH 25/50] revert back limit parameter renaming and optimization to make for easier cross-reference with AbstractSqlAstTranslator --- .../translate/AbstractMqlTranslator.java | 47 +++++++++++++------ .../translate/SelectMqlTranslator.java | 4 +- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index f753b097..a066914e 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -85,7 +85,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import org.bson.BsonBoolean; import org.bson.BsonDecimal128; import org.bson.BsonDouble; @@ -210,9 +209,9 @@ abstract class AbstractMqlTranslator implements SqlAstT @Nullable Limit limit; - @Nullable JdbcParameter firstRowJdbcParameter; + @Nullable JdbcParameter offsetParameter; - @Nullable JdbcParameter maxRowsJdbcParameter; + @Nullable JdbcParameter limitParameter; AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; @@ -598,13 +597,13 @@ private List createSkipLimitStages(QueryPart queryPart) { if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); if (limit.getFirstRow() != null) { - firstRowJdbcParameter = new LimitJdbcParameter(basicIntegerType, Limit::getFirstRow); + offsetParameter = new OffsetJdbcParameter(basicIntegerType); } if (limit.getMaxRows() != null) { - maxRowsJdbcParameter = new LimitJdbcParameter(basicIntegerType, Limit::getMaxRows); + limitParameter = new LimitJdbcParameter(basicIntegerType); } - skipExpression = firstRowJdbcParameter; - limitExpression = maxRowsJdbcParameter; + skipExpression = offsetParameter; + limitExpression = limitParameter; } else { if (queryPart.getFetchClauseType() != ROWS_ONLY) { throw new FeatureNotSupportedException(format( @@ -1026,17 +1025,38 @@ private static BsonValue toBsonValue(@Nullable Object queryLiteral) { throw new FeatureNotSupportedException("Unsupported Java type: " + queryLiteral.getClass()); } - private static final class LimitJdbcParameter extends AbstractJdbcParameter { + private static class OffsetJdbcParameter extends AbstractJdbcParameter { - private final Function limitValueAccess; - - public LimitJdbcParameter(BasicType type, Function limitValueAccess) { + public OffsetJdbcParameter(BasicType type) { super(type); - this.limitValueAccess = limitValueAccess; } + @Override @SuppressWarnings("unchecked") + public void bindParameterValue( + PreparedStatement statement, + int startPosition, + JdbcParameterBindings jdbcParamBindings, + ExecutionContext executionContext) + throws SQLException { + getJdbcMapping() + .getJdbcValueBinder() + .bind( + statement, + executionContext.getQueryOptions().getLimit().getFirstRow(), + startPosition, + executionContext.getSession()); + } + } + + private static class LimitJdbcParameter extends AbstractJdbcParameter { + + public LimitJdbcParameter(BasicType type) { + super(type); + } + @Override + @SuppressWarnings("unchecked") public void bindParameterValue( PreparedStatement statement, int startPosition, @@ -1047,8 +1067,7 @@ public void bindParameterValue( .getJdbcValueBinder() .bind( statement, - limitValueAccess.apply( - executionContext.getQueryOptions().getLimit()), + executionContext.getQueryOptions().getLimit().getMaxRows(), startPosition, executionContext.getSession()); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 9778a1a0..9bc68f16 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -69,7 +69,7 @@ public JdbcOperationQuerySelect translate( MAX_VALUE, emptyMap(), NONE, - firstRowJdbcParameter, - maxRowsJdbcParameter); + offsetParameter, + limitParameter); } } From 3b9ca8d06747b08886f2cf5b3653b0338a1f386f Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 26 May 2025 10:25:30 -0400 Subject: [PATCH 26/50] add query validation to QueryPlanCacheTests --- ...imitOffsetFetchClauseIntegrationTests.java | 114 ++++++++++++++---- 1 file changed, 92 insertions(+), 22 deletions(-) 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 5632ad94..331c36c4 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -18,6 +18,7 @@ import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; 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; @@ -29,6 +30,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.bson.BsonDocument; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -451,6 +453,32 @@ void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { class QueryPlanCacheTests extends AbstractSelectionQueryIntegrationTests { private static final String HQL = "from Book order by id"; + private static final String expectedMqlTemplate = + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + %s + %s + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """; private TranslatingCacheTestingDialect translatingCacheTestingDialect; @@ -460,6 +488,7 @@ void beforeEach() { .getSessionFactory() .getJdbcServices() .getDialect(); + getTestCommandListener().clear(); } @ParameterizedTest @@ -467,45 +496,56 @@ void beforeEach() { void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsSet) { getSessionFactoryScope().inTransaction(session -> { var firstQuery = session.createSelectionQuery(HQL, Book.class); - setQueryOptionsAndQuery(firstQuery, isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null); + setQueryOptionsAndQuery( + firstQuery, + isFirstResultSet ? 5 : null, + isMaxResultsSet ? 10 : null, + format( + expectedMqlTemplate, + (isFirstResultSet ? "{\"$skip\": 5}," : ""), + (isMaxResultsSet ? "{\"$limit\": 10}," : ""))); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); var secondQuery = session.createSelectionQuery(HQL, Book.class); - setQueryOptionsAndQuery(secondQuery, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null); + setQueryOptionsAndQuery( + secondQuery, + isFirstResultSet ? 3 : null, + isMaxResultsSet ? 6 : null, + format( + expectedMqlTemplate, + (isFirstResultSet ? "{\"$skip\": 3}," : ""), + (isMaxResultsSet ? "{\"$limit\": 6}," : ""))); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount); }); } - private static void setQueryOptionsAndQuery( - SelectionQuery query, Integer firstResult, Integer maxResults) { - if (firstResult != null) { - query.setFirstResult(firstResult); - } - if (maxResults != null) { - query.setMaxResults(maxResults); - } - query.getResultList(); - } - @Test void testCacheInvalidatedDueToQueryOptionsAdded() { getSessionFactoryScope().inTransaction(session -> { - session.createSelectionQuery(HQL, Book.class).getResultList(); + var query = session.createSelectionQuery(HQL, Book.class); + setQueryOptionsAndQuery(query, null, null, format(expectedMqlTemplate, "", "")); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); - session.createSelectionQuery(HQL, Book.class).setFirstResult(1).getResultList(); + var queryWithOffsetQueryOption = + session.createSelectionQuery(HQL, Book.class).setFirstResult(1); + setQueryOptionsAndQuery( + queryWithOffsetQueryOption, 1, null, format(expectedMqlTemplate, "{\"$skip\": 1},", "")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); - session.createSelectionQuery(HQL, Book.class) + var queryWithBothOffsetAndLimitQueryOptions = session.createSelectionQuery(HQL, Book.class) .setFirstResult(1) - .setMaxResults(5) - .getResultList(); + .setMaxResults(5); + setQueryOptionsAndQuery( + queryWithBothOffsetAndLimitQueryOptions, + 1, + 5, + format(expectedMqlTemplate, "{\"$skip\": 1},", "{\"$limit\": 5},")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 2); }); @@ -514,12 +554,18 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { @Test void testCacheInvalidatedDueToQueryOptionsRemoved() { getSessionFactoryScope().inTransaction(session -> { - session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); + var queryWithOffsetQueryOption = + session.createSelectionQuery(HQL, Book.class).setFirstResult(10); + setQueryOptionsAndQuery( + queryWithOffsetQueryOption, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); - session.createSelectionQuery(HQL, Book.class).getResultList(); + var queryWithoutOffsetQueryOption = session.createSelectionQuery(HQL, Book.class); + setQueryOptionsAndQuery(queryWithoutOffsetQueryOption, null, null, format(expectedMqlTemplate, "", "")); + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); }); @@ -528,16 +574,40 @@ void testCacheInvalidatedDueToQueryOptionsRemoved() { @Test void testCacheInvalidatedDueToDifferentQueryOptions() { getSessionFactoryScope().inTransaction(session -> { - session.createSelectionQuery(HQL, Book.class).setFirstResult(10).getResultList(); + var queryWithOffsetQueryOption = + session.createSelectionQuery(HQL, Book.class).setFirstResult(10); + setQueryOptionsAndQuery( + queryWithOffsetQueryOption, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); - session.createSelectionQuery(HQL, Book.class).setMaxResults(20).getResultList(); + var queryWithLimitQueryOption = + session.createSelectionQuery(HQL, Book.class).setMaxResults(20); + setQueryOptionsAndQuery( + queryWithLimitQueryOption, null, 20, format(expectedMqlTemplate, "", "{\"$limit\": 20},")); + assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); }); } + + private void setQueryOptionsAndQuery( + SelectionQuery query, Integer firstResult, Integer maxResults, String expectedMql) { + if (firstResult != null) { + query.setFirstResult(firstResult); + } + if (maxResults != null) { + query.setMaxResults(maxResults); + } + getTestCommandListener().clear(); + query.getResultList(); + if (expectedMql != null) { + var expectedCommand = BsonDocument.parse(expectedMql); + assertActualCommand(expectedCommand); + } + } } public static final class TranslatingCacheTestingDialect extends Dialect { From 341826a4d559b4e31342ad3c566225dfdc958668 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 26 May 2025 15:50:44 -0400 Subject: [PATCH 27/50] add more comments to explain some subtle details --- .../select/LimitOffsetFetchClauseIntegrationTests.java | 6 ++++++ .../hibernate/internal/translate/SelectMqlTranslator.java | 1 + 2 files changed, 7 insertions(+) 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 331c36c4..c1394b95 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -610,6 +610,12 @@ private void setQueryOptionsAndQuery( } } + /** + * A dialect that counts how many times the select translator is created. This is used to test the query plan cache. + * Note that {@code QueryStatistics#getQueryCacheCount()} or {@code QueryStatistics#getPlanCacheMissCount()} is not + * used here, because it counts the number of times the query cache is hit, not whether new translator is created + * (e.g., incompatible {@code QueryOptions} might end up with new translation. + */ public static final class TranslatingCacheTestingDialect extends Dialect { private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); private final Dialect delegate; diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 9bc68f16..6d7e3623 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -69,6 +69,7 @@ public JdbcOperationQuerySelect translate( MAX_VALUE, emptyMap(), NONE, + // the following parameters are provided for query plan cache purposes offsetParameter, limitParameter); } From 8ed3bbf76ec60be0c075b94ecd7a6b2c24008b2f Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 26 May 2025 17:26:10 -0400 Subject: [PATCH 28/50] fine-tune javadoc for TranslatingCacheTestingDialect in LimitOffsetFetchClauseIntegrationTests --- .../LimitOffsetFetchClauseIntegrationTests.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 c1394b95..068fbc63 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -44,6 +44,7 @@ import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.model.ast.TableMutation; import org.hibernate.sql.model.jdbc.JdbcMutationOperation; +import org.hibernate.stat.QueryStatistics; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.Setting; @@ -572,7 +573,7 @@ void testCacheInvalidatedDueToQueryOptionsRemoved() { } @Test - void testCacheInvalidatedDueToDifferentQueryOptions() { + void testCacheInvalidatedDueToQueryOptionsChanged() { getSessionFactoryScope().inTransaction(session -> { var queryWithOffsetQueryOption = session.createSelectionQuery(HQL, Book.class).setFirstResult(10); @@ -611,10 +612,11 @@ private void setQueryOptionsAndQuery( } /** - * A dialect that counts how many times the select translator is created. This is used to test the query plan cache. - * Note that {@code QueryStatistics#getQueryCacheCount()} or {@code QueryStatistics#getPlanCacheMissCount()} is not - * used here, because it counts the number of times the query cache is hit, not whether new translator is created - * (e.g., incompatible {@code QueryOptions} might end up with new translation. + * A dialect that counts how many times the select translator is created. + * + *

Note that {@link QueryStatistics#getPlanCacheHitCount()} is not used, because it counts the number of times + * the query plan cache is hit, not whether {@link SqlAstTranslator} is reused afterwards (e.g., incompatible + * {@link org.hibernate.query.spi.QueryOptions QueryOptions}s will end up with new translator bing created). */ public static final class TranslatingCacheTestingDialect extends Dialect { private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); From b6845a0209b887c67602d77fc0538b2a840a58e1 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 27 May 2025 09:46:20 -0400 Subject: [PATCH 29/50] Update src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java Co-authored-by: Viacheslav Babanin --- .../hibernate/internal/translate/SelectMqlTranslator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 6d7e3623..0ef9a060 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -69,7 +69,8 @@ public JdbcOperationQuerySelect translate( MAX_VALUE, emptyMap(), NONE, - // the following parameters are provided for query plan cache purposes + // The following parameters are provided for query plan cache purposes. + // Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. offsetParameter, limitParameter); } From cf28b15e67e5a435d7431993ee7d214003b79e88 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Tue, 27 May 2025 09:49:47 -0400 Subject: [PATCH 30/50] resolve Slava's code review comments --- ...imitOffsetFetchClauseIntegrationTests.java | 177 +++++++++--------- .../translate/AbstractMqlTranslator.java | 84 ++++----- .../translate/SelectMqlTranslator.java | 6 +- 3 files changed, 135 insertions(+), 132 deletions(-) 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 068fbc63..3ca43e91 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -16,7 +16,6 @@ package com.mongodb.hibernate.query.select; -import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static java.lang.String.format; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -31,10 +30,10 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.bson.BsonDocument; +import org.hibernate.Session; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.query.SelectionQuery; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -54,6 +53,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) class LimitOffsetFetchClauseIntegrationTests extends AbstractSelectionQueryIntegrationTests { @@ -199,10 +199,15 @@ void testHqlLimitAndOffsetClauses() { getBooksByIds(3, 4)); } - @Test - void testHqlFetchClauseOnly() { + @ParameterizedTest + @ValueSource( + strings = { + "FETCH FIRST :limit ROWS ONLY", + "FETCH NEXT :limit ROWS ONLY", + }) + void testHqlFetchClauseOnly(final String fetchClause) { assertSelectionQuery( - "from Book order by id FETCH FIRST :limit ROWS ONLY", + "from Book order by id " + fetchClause, Book.class, q -> q.setParameter("limit", 5), """ @@ -356,61 +361,80 @@ void testQueryOptionsSetFirstResultAndMaxResults() { @Nested class WithHqlClauses { - @ParameterizedTest - @CsvSource({"true,false", "false,true", "true,true"}) - void testWithDirectConflicts(boolean isFirstResultSet, boolean isMaxResultsSet) { - assertTrue(isFirstResultSet || isMaxResultsSet); + private static final String expectedMqlTemplate = + """ + { + "aggregate": "books", + "pipeline": [ + { + "$sort": { + "_id": 1 + } + }, + %s, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """; + + @Test + void testFirstResultConflictingOnly() { var firstResult = 5; + var expectedBooks = getBooksByIds(5, 6, 7, 8, 9); + assertSelectionQuery( + "from Book order by id LIMIT :limit OFFSET :offset", + Book.class, + q -> + // hql clauses will be ignored totally + q.setParameter("limit", 10) + .setParameter("offset", 0) + .setFirstResult(firstResult), + expectedMqlTemplate.formatted("{\"$skip\": " + firstResult + "}"), + expectedBooks); + } + + @Test + void testMaxResultsConflictingOnly() { var maxResults = 3; - final List expectedBooks; - if (!isMaxResultsSet) { - expectedBooks = getBooksByIds(5, 6, 7, 8, 9); // firstResult: 5 - } else if (!isFirstResultSet) { - expectedBooks = getBooksByIds(0, 1, 2); // maxResults: 3 - } else { - expectedBooks = getBooksByIds(5, 6, 7); // firstResult: 5 && maxResults: 3 - } + var expectedBooks = getBooksByIds(0, 1, 2); assertSelectionQuery( "from Book order by id LIMIT :limit OFFSET :offset", Book.class, - q -> { - q.setParameter("limit", 10) - .setParameter("offset", 0); // hql clauses will be ignored totally - if (isFirstResultSet) { - q.setFirstResult(firstResult); - } - if (isMaxResultsSet) { - q.setMaxResults(maxResults); - } - }, - """ - { - "aggregate": "books", - "pipeline": [ - { - "$sort": { - "_id": 1 - } - }, - %s - %s - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - } - """ - .formatted( - (isFirstResultSet ? "{\"$skip\": " + firstResult + "}," : ""), - (isMaxResultsSet ? "{\"$limit\": " + maxResults + "}," : "")), + q -> + // hql clauses will be ignored totally + q.setParameter("limit", 10) + .setParameter("offset", 0) + .setMaxResults(maxResults), + expectedMqlTemplate.formatted("{\"$limit\": " + maxResults + "}"), + expectedBooks); + } + + @Test + void testBothFirstResultAndMaxResultsConflicting() { + var firstResult = 5; + var maxResults = 3; + var expectedBooks = getBooksByIds(5, 6, 7); + assertSelectionQuery( + "from Book order by id LIMIT :limit OFFSET :offset", + Book.class, + q -> + // hql clauses will be ignored totally + q.setParameter("limit", 10) + .setParameter("offset", 0) + .setFirstResult(firstResult) + .setMaxResults(maxResults), + expectedMqlTemplate.formatted( + "{\"$skip\": " + firstResult + "}," + "{\"$limit\": " + maxResults + "}"), expectedBooks); } } @@ -496,9 +520,8 @@ void beforeEach() { @CsvSource({"true,false", "false,true", "true,true"}) void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsSet) { getSessionFactoryScope().inTransaction(session -> { - var firstQuery = session.createSelectionQuery(HQL, Book.class); setQueryOptionsAndQuery( - firstQuery, + session, isFirstResultSet ? 5 : null, isMaxResultsSet ? 10 : null, format( @@ -509,9 +532,8 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS assertThat(initialSelectTranslatingCount).isPositive(); - var secondQuery = session.createSelectionQuery(HQL, Book.class); setQueryOptionsAndQuery( - secondQuery, + session, isFirstResultSet ? 3 : null, isMaxResultsSet ? 6 : null, format( @@ -526,27 +548,17 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS @Test void testCacheInvalidatedDueToQueryOptionsAdded() { getSessionFactoryScope().inTransaction(session -> { - var query = session.createSelectionQuery(HQL, Book.class); - setQueryOptionsAndQuery(query, null, null, format(expectedMqlTemplate, "", "")); + setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); - var queryWithOffsetQueryOption = - session.createSelectionQuery(HQL, Book.class).setFirstResult(1); - setQueryOptionsAndQuery( - queryWithOffsetQueryOption, 1, null, format(expectedMqlTemplate, "{\"$skip\": 1},", "")); + setQueryOptionsAndQuery(session, 1, null, format(expectedMqlTemplate, "{\"$skip\": 1},", "")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); - var queryWithBothOffsetAndLimitQueryOptions = session.createSelectionQuery(HQL, Book.class) - .setFirstResult(1) - .setMaxResults(5); setQueryOptionsAndQuery( - queryWithBothOffsetAndLimitQueryOptions, - 1, - 5, - format(expectedMqlTemplate, "{\"$skip\": 1},", "{\"$limit\": 5},")); + session, 1, 5, format(expectedMqlTemplate, "{\"$skip\": 1},", "{\"$limit\": 5},")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 2); }); @@ -555,17 +567,13 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { @Test void testCacheInvalidatedDueToQueryOptionsRemoved() { getSessionFactoryScope().inTransaction(session -> { - var queryWithOffsetQueryOption = - session.createSelectionQuery(HQL, Book.class).setFirstResult(10); - setQueryOptionsAndQuery( - queryWithOffsetQueryOption, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); + setQueryOptionsAndQuery(session, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); - var queryWithoutOffsetQueryOption = session.createSelectionQuery(HQL, Book.class); - setQueryOptionsAndQuery(queryWithoutOffsetQueryOption, null, null, format(expectedMqlTemplate, "", "")); + setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); @@ -575,19 +583,13 @@ void testCacheInvalidatedDueToQueryOptionsRemoved() { @Test void testCacheInvalidatedDueToQueryOptionsChanged() { getSessionFactoryScope().inTransaction(session -> { - var queryWithOffsetQueryOption = - session.createSelectionQuery(HQL, Book.class).setFirstResult(10); - setQueryOptionsAndQuery( - queryWithOffsetQueryOption, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); + setQueryOptionsAndQuery(session, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); assertThat(initialSelectTranslatingCount).isPositive(); - var queryWithLimitQueryOption = - session.createSelectionQuery(HQL, Book.class).setMaxResults(20); - setQueryOptionsAndQuery( - queryWithLimitQueryOption, null, 20, format(expectedMqlTemplate, "", "{\"$limit\": 20},")); + setQueryOptionsAndQuery(session, null, 20, format(expectedMqlTemplate, "", "{\"$limit\": 20},")); assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) .isEqualTo(initialSelectTranslatingCount + 1); @@ -595,7 +597,8 @@ void testCacheInvalidatedDueToQueryOptionsChanged() { } private void setQueryOptionsAndQuery( - SelectionQuery query, Integer firstResult, Integer maxResults, String expectedMql) { + Session session, Integer firstResult, Integer maxResults, String expectedMql) { + var query = session.createSelectionQuery(HQL, Book.class); if (firstResult != null) { query.setFirstResult(firstResult); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index a066914e..e6f93955 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -207,11 +207,11 @@ abstract class AbstractMqlTranslator implements SqlAstT private final Set affectedTableNames = new HashSet<>(); - @Nullable Limit limit; + @Nullable Limit queryOptionsLimit; - @Nullable JdbcParameter offsetParameter; + @Nullable JdbcParameter queryOptionsOffsetParameter; - @Nullable JdbcParameter limitParameter; + @Nullable JdbcParameter queryOptionsLimitParameter; AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; @@ -404,6 +404,45 @@ private List createSkipLimitStages(QuerySpec querySpec) { return astVisitorValueHolder.execute(SKIP_LIMIT_STAGES, () -> visitOffsetFetchClause(querySpec)); } + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + var skipLimitStages = createSkipLimitStages(queryPart); + astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, skipLimitStages); + } + + private List createSkipLimitStages(QueryPart queryPart) { + var skipLimitStages = new ArrayList(2); + final Expression skipExpression; + final Expression limitExpression; + if (queryPart.isRoot() && queryOptionsLimit != null && !queryOptionsLimit.isEmpty()) { + var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); + if (queryOptionsLimit.getFirstRow() != null) { + queryOptionsOffsetParameter = new OffsetJdbcParameter(basicIntegerType); + } + if (queryOptionsLimit.getMaxRows() != null) { + queryOptionsLimitParameter = new LimitJdbcParameter(basicIntegerType); + } + skipExpression = queryOptionsOffsetParameter; + limitExpression = queryOptionsLimitParameter; + } else { + if (queryPart.getFetchClauseType() != ROWS_ONLY) { + throw new FeatureNotSupportedException(format( + "%s does not support '%s' fetch clause type", MONGO_DBMS_NAME, queryPart.getFetchClauseType())); + } + skipExpression = queryPart.getOffsetClauseExpression(); + limitExpression = queryPart.getFetchClauseExpression(); + } + if (skipExpression != null) { + var skipValue = acceptAndYield(skipExpression, FIELD_VALUE); + skipLimitStages.add(new AstSkipStage(skipValue)); + } + if (limitExpression != null) { + var limitValue = acceptAndYield(limitExpression, FIELD_VALUE); + skipLimitStages.add(new AstLimitStage(limitValue)); + } + return skipLimitStages; + } + private AstProjectStage createProjectStage(SelectClause selectClause) { var projectStageSpecifications = acceptAndYield(selectClause, PROJECT_STAGE_SPECIFICATIONS); return new AstProjectStage(projectStageSpecifications); @@ -584,45 +623,6 @@ private AstSortField createAstSortField(Expression sortExpression, AstSortOrder return new AstSortField(fieldPath, astSortOrder); } - @Override - public void visitOffsetFetchClause(QueryPart queryPart) { - var skipLimitStages = createSkipLimitStages(queryPart); - astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, skipLimitStages); - } - - private List createSkipLimitStages(QueryPart queryPart) { - var skipLimitStages = new ArrayList(2); - final Expression skipExpression; - final Expression limitExpression; - if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { - var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); - if (limit.getFirstRow() != null) { - offsetParameter = new OffsetJdbcParameter(basicIntegerType); - } - if (limit.getMaxRows() != null) { - limitParameter = new LimitJdbcParameter(basicIntegerType); - } - skipExpression = offsetParameter; - limitExpression = limitParameter; - } else { - if (queryPart.getFetchClauseType() != ROWS_ONLY) { - throw new FeatureNotSupportedException(format( - "%s does not support '%s' fetch clause type", MONGO_DBMS_NAME, queryPart.getFetchClauseType())); - } - skipExpression = queryPart.getOffsetClauseExpression(); - limitExpression = queryPart.getFetchClauseExpression(); - } - if (skipExpression != null) { - var skipValue = acceptAndYield(skipExpression, FIELD_VALUE); - skipLimitStages.add(new AstSkipStage(skipValue)); - } - if (limitExpression != null) { - var limitValue = acceptAndYield(limitExpression, FIELD_VALUE); - skipLimitStages.add(new AstLimitStage(limitValue)); - } - return skipLimitStages; - } - @Override public void visitTuple(SqlTuple sqlTuple) { var expressions = new ArrayList(sqlTuple.getExpressions().size()); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 0ef9a060..b1fe8d18 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -53,7 +53,7 @@ public JdbcOperationQuerySelect translate( checkQueryOptionsSupportability(queryOptions); if (queryOptions.getLimit() != null) { - limit = queryOptions.getLimit().makeCopy(); + queryOptionsLimit = queryOptions.getLimit().makeCopy(); } var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE); @@ -71,7 +71,7 @@ public JdbcOperationQuerySelect translate( NONE, // The following parameters are provided for query plan cache purposes. // Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. - offsetParameter, - limitParameter); + queryOptionsOffsetParameter, + queryOptionsLimitParameter); } } From 06c39fddc9f3ec02380634d00f33bb028529ee27 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Wed, 28 May 2025 09:28:07 -0400 Subject: [PATCH 31/50] merge in main branch --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index b23e55cd..ae131173 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -77,9 +77,9 @@ import com.mongodb.hibernate.internal.type.ValueConversions; import java.io.IOException; import java.io.StringWriter; -import java.sql.SQLFeatureNotSupportedException; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; From 65c6cdf2e5209a1f3d81a4c7b35e2b20d26a5249 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 09:51:04 -0400 Subject: [PATCH 32/50] rename AstVisitorValueDescriptor.FIELD_VALUE to VALUE --- .../translate/AbstractMqlTranslator.java | 20 +++++++++---------- .../translate/AstVisitorValueDescriptor.java | 3 ++- .../translate/AstVisitorValueHolderTests.java | 20 +++++++++---------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index ae131173..8db99f99 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -24,12 +24,12 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATH; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SKIP_LIMIT_STAGES; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.TUPLE; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.VALUE; import static com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue.FALSE; import static com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue.TRUE; import static com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortOrder.ASC; @@ -270,7 +270,7 @@ public void visitStandardTableInsert(TableInsertStandard tableInsert) { if (valueExpression == null) { throw new FeatureNotSupportedException(); } - var fieldValue = acceptAndYield(valueExpression, FIELD_VALUE); + var fieldValue = acceptAndYield(valueExpression, VALUE); astElements.add(new AstElement(fieldName, fieldValue)); } astVisitorValueHolder.yield( @@ -309,7 +309,7 @@ public void visitStandardTableUpdate(TableUpdateStandard tableUpdate) { var updates = new ArrayList(tableUpdate.getNumberOfValueBindings()); for (var valueBinding : tableUpdate.getValueBindings()) { var fieldName = valueBinding.getColumnReference().getColumnExpression(); - var fieldValue = acceptAndYield(valueBinding.getValueExpression(), FIELD_VALUE); + var fieldValue = acceptAndYield(valueBinding.getValueExpression(), VALUE); updates.add(new AstFieldUpdate(fieldName, fieldValue)); } astVisitorValueHolder.yield( @@ -330,14 +330,14 @@ private AstFilter getKeyFilter(AbstractRestrictedTableMutation createSkipLimitStages(QueryPart queryPart) { limitExpression = queryPart.getFetchClauseExpression(); } if (skipExpression != null) { - var skipValue = acceptAndYield(skipExpression, FIELD_VALUE); + var skipValue = acceptAndYield(skipExpression, VALUE); skipLimitStages.add(new AstSkipStage(skipValue)); } if (limitExpression != null) { - var limitValue = acceptAndYield(limitExpression, FIELD_VALUE); + var limitValue = acceptAndYield(limitExpression, VALUE); skipLimitStages.add(new AstLimitStage(limitValue)); } return skipLimitStages; @@ -478,7 +478,7 @@ public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) { } var fieldPath = acceptAndYield((isFieldOnLeftHandSide ? lhs : rhs), FIELD_PATH); - var comparisonValue = acceptAndYield((isFieldOnLeftHandSide ? rhs : lhs), FIELD_VALUE); + var comparisonValue = acceptAndYield((isFieldOnLeftHandSide ? rhs : lhs), VALUE); var operator = isFieldOnLeftHandSide ? comparisonPredicate.getOperator() @@ -537,7 +537,7 @@ public void visitQueryLiteral(QueryLiteral queryLiteral) { if (literalValue == null) { throw new FeatureNotSupportedException("TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74"); } - astVisitorValueHolder.yield(FIELD_VALUE, new AstLiteralValue(toBsonValue(literalValue))); + astVisitorValueHolder.yield(VALUE, new AstLiteralValue(toBsonValue(literalValue))); } @Override @@ -557,7 +557,7 @@ public void visitJunction(Junction junction) { @Override public void visitUnparsedNumericLiteral(UnparsedNumericLiteral unparsedNumericLiteral) { var literalValue = assertNotNull(unparsedNumericLiteral.getLiteralValue()); - astVisitorValueHolder.yield(FIELD_VALUE, new AstLiteralValue(toBsonValue(literalValue))); + astVisitorValueHolder.yield(VALUE, new AstLiteralValue(toBsonValue(literalValue))); } @Override diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index 7bb80b04..cf565a37 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -41,7 +41,6 @@ final class AstVisitorValueDescriptor { static final AstVisitorValueDescriptor COLLECTION_NAME = new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor FIELD_PATH = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor FIELD_VALUE = new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor> PROJECT_STAGE_SPECIFICATIONS = new AstVisitorValueDescriptor<>(); @@ -53,6 +52,8 @@ final class AstVisitorValueDescriptor { static final AstVisitorValueDescriptor> SKIP_LIMIT_STAGES = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor VALUE = new AstVisitorValueDescriptor<>(); + private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; static { diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java index b11d20aa..2f4d09aa 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java @@ -17,7 +17,7 @@ package com.mongodb.hibernate.internal.translate; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_VALUE; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.VALUE; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -45,9 +45,9 @@ void beforeEach() { void testSimpleUsage() { var value = new AstLiteralValue(new BsonString("field_value")); - Runnable valueYielder = () -> astVisitorValueHolder.yield(FIELD_VALUE, value); + Runnable valueYielder = () -> astVisitorValueHolder.yield(VALUE, value); - var valueGotten = astVisitorValueHolder.execute(FIELD_VALUE, valueYielder); + var valueGotten = astVisitorValueHolder.execute(VALUE, valueYielder); assertSame(value, valueGotten); } @@ -57,9 +57,9 @@ void testRecursiveUsage() { Runnable tableInserter = () -> { Runnable fieldValueYielder = () -> { - astVisitorValueHolder.yield(FIELD_VALUE, AstParameterMarker.INSTANCE); + astVisitorValueHolder.yield(VALUE, AstParameterMarker.INSTANCE); }; - var fieldValue = astVisitorValueHolder.execute(FIELD_VALUE, fieldValueYielder); + var fieldValue = astVisitorValueHolder.execute(VALUE, fieldValueYielder); AstElement astElement = new AstElement("province", fieldValue); astVisitorValueHolder.yield( COLLECTION_MUTATION, new AstInsertCommand("city", new AstDocument(List.of(astElement)))); @@ -73,11 +73,11 @@ void testRecursiveUsage() { void testHolderNotEmptyWhenSetting() { Runnable valueYielder = () -> { - astVisitorValueHolder.yield(FIELD_VALUE, new AstLiteralValue(new BsonString("value1"))); - astVisitorValueHolder.yield(FIELD_VALUE, new AstLiteralValue(new BsonString("value2"))); + astVisitorValueHolder.yield(VALUE, new AstLiteralValue(new BsonString("value1"))); + astVisitorValueHolder.yield(VALUE, new AstLiteralValue(new BsonString("value2"))); }; - assertThrows(Error.class, () -> astVisitorValueHolder.execute(FIELD_VALUE, valueYielder)); + assertThrows(Error.class, () -> astVisitorValueHolder.execute(VALUE, valueYielder)); } @Test @@ -85,7 +85,7 @@ void testHolderNotEmptyWhenSetting() { void testHolderExpectingDifferentDescriptor() { Runnable valueYielder = - () -> astVisitorValueHolder.yield(FIELD_VALUE, new AstLiteralValue(new BsonString("some_value"))); + () -> astVisitorValueHolder.yield(VALUE, new AstLiteralValue(new BsonString("some_value"))); assertThrows(Error.class, () -> astVisitorValueHolder.execute(COLLECTION_MUTATION, valueYielder)); } @@ -93,6 +93,6 @@ void testHolderExpectingDifferentDescriptor() { @Test @DisplayName("Exception is thrown when no value is yielded") void testHolderStillEmpty() { - assertThrows(Error.class, () -> astVisitorValueHolder.execute(FIELD_VALUE, () -> {})); + assertThrows(Error.class, () -> astVisitorValueHolder.execute(VALUE, () -> {})); } } From 91c80aaab1bb15851ba3c4f417ecc5f5520fe0aa Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 09:59:48 -0400 Subject: [PATCH 33/50] add literal parameter testing to LimitOffsetFetchClauseIntegrationTests --- ...imitOffsetFetchClauseIntegrationTests.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) 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 3ca43e91..b8b9e7ee 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -88,12 +88,13 @@ void beforeEach() { @Nested class WithoutQueryOptionsLimit { - @Test - void testHqlLimitClauseOnly() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testHqlLimitClauseOnly(boolean useLiteralParameter) { assertSelectionQuery( - "from Book order by id LIMIT :limit", + useLiteralParameter ? "from Book order by id LIMIT 5" : "from Book order by id LIMIT :limit", Book.class, - q -> q.setParameter("limit", 5), + useLiteralParameter ? null : q -> q.setParameter("limit", 5), """ { "aggregate": "books", @@ -124,12 +125,13 @@ void testHqlLimitClauseOnly() { getBooksByIds(0, 1, 2, 3, 4)); } - @Test - void testHqlOffsetClauseOnly() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testHqlOffsetClauseOnly(boolean useLiteralParameter) { assertSelectionQuery( - "from Book order by id OFFSET :offset", + useLiteralParameter ? "from Book order by id OFFSET 7" : "from Book order by id OFFSET :offset", Book.class, - q -> q.setParameter("offset", 7), + useLiteralParameter ? null : q -> q.setParameter("offset", 7), """ { "aggregate": "books", @@ -160,12 +162,17 @@ void testHqlOffsetClauseOnly() { getBooksByIds(7, 8, 9)); } - @Test - void testHqlLimitAndOffsetClauses() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testHqlLimitAndOffsetClauses(boolean useLiteralParameters) { assertSelectionQuery( - "from Book order by id LIMIT :limit OFFSET :offset", + useLiteralParameters + ? "from Book order by id LIMIT 2 OFFSET 3" + : "from Book order by id LIMIT :limit OFFSET :offset", Book.class, - q -> q.setParameter("offset", 3).setParameter("limit", 2), + useLiteralParameters + ? null + : q -> q.setParameter("offset", 3).setParameter("limit", 2), """ { "aggregate": "books", @@ -205,7 +212,7 @@ void testHqlLimitAndOffsetClauses() { "FETCH FIRST :limit ROWS ONLY", "FETCH NEXT :limit ROWS ONLY", }) - void testHqlFetchClauseOnly(final String fetchClause) { + void testHqlFetchClauseOnly(String fetchClause) { assertSelectionQuery( "from Book order by id " + fetchClause, Book.class, From 0440a9f321a5eb57723eeb1657036a60e1c75f96 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:07:56 -0400 Subject: [PATCH 34/50] Update src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java Co-authored-by: Valentin Kovalenko --- .../hibernate/internal/translate/SelectMqlTranslator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index b1fe8d18..1f99c3d4 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -52,8 +52,9 @@ public JdbcOperationQuerySelect translate( checkJdbcParameterBindingsSupportability(jdbcParameterBindings); checkQueryOptionsSupportability(queryOptions); - if (queryOptions.getLimit() != null) { - queryOptionsLimit = queryOptions.getLimit().makeCopy(); + var limit = queryOptions.getLimit(); + if (limit != null) { + queryOptionsLimit = limit.makeCopy(); } var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE); From f5e6d223b2b299e07697c92be94270b0d31a62a0 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:31:58 -0400 Subject: [PATCH 35/50] simplify AbstractMqlTranslator by removing unnecessary implementation of visitOffsetFetchClause() --- .../internal/translate/AbstractMqlTranslator.java | 9 ++------- .../internal/translate/AstVisitorValueDescriptor.java | 3 --- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 8db99f99..4dbb0c49 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -18,6 +18,7 @@ import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; +import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static com.mongodb.hibernate.internal.MongoConstants.EXTENDED_JSON_WRITER_SETTINGS; import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; @@ -26,7 +27,6 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATH; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SKIP_LIMIT_STAGES; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.TUPLE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.VALUE; @@ -393,14 +393,9 @@ private Optional createSortStage(QuerySpec querySpec) { return Optional.empty(); } - private List createSkipLimitStages(QuerySpec querySpec) { - return astVisitorValueHolder.execute(SKIP_LIMIT_STAGES, () -> visitOffsetFetchClause(querySpec)); - } - @Override public void visitOffsetFetchClause(QueryPart queryPart) { - var skipLimitStages = createSkipLimitStages(queryPart); - astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, skipLimitStages); + fail("There is no code in Hibernate ORM that calls this method"); } private List createSkipLimitStages(QueryPart queryPart) { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index cf565a37..7a193a59 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -23,7 +23,6 @@ import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; -import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstStage; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter; import java.lang.reflect.Modifier; import java.util.Collections; @@ -50,8 +49,6 @@ final class AstVisitorValueDescriptor { static final AstVisitorValueDescriptor> TUPLE = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor> SKIP_LIMIT_STAGES = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor VALUE = new AstVisitorValueDescriptor<>(); private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; From d30e5069282df9d31ddaa146e39ec7f7c5bf63a0 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:34:02 -0400 Subject: [PATCH 36/50] revert back deletion of QueryOptions.getFetchSize() validation in AbstractMqlTranslator --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 4dbb0c49..9a9eb966 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -958,6 +958,9 @@ static void checkQueryOptionsSupportability(QueryOptions queryOptions) { && !queryOptions.getDatabaseHints().isEmpty()) { throw new FeatureNotSupportedException("'databaseHints' in QueryOptions not supported"); } + if (queryOptions.getFetchSize() != null) { + throw new FeatureNotSupportedException("TODO-HIBERNATE-54 https://jira.mongodb.org/browse/HIBERNATE-54"); + } } private static AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) { From 81b6693f2a7c39087ba56a0f758a2bd0b1d9fa72 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:35:48 -0400 Subject: [PATCH 37/50] Update src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java Co-authored-by: Valentin Kovalenko --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index ae131173..72d0e6b9 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -1004,7 +1004,7 @@ private static BsonValue toBsonValue(Object value) { } } - private static class OffsetJdbcParameter extends AbstractJdbcParameter { + private static final class OffsetJdbcParameter extends AbstractJdbcParameter { public OffsetJdbcParameter(BasicType type) { super(type); From 98847f091975599a46d4ebb3b19d7e7a5b476ab5 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:36:02 -0400 Subject: [PATCH 38/50] Update src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java Co-authored-by: Valentin Kovalenko --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 72d0e6b9..414be3e1 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -1028,7 +1028,7 @@ public void bindParameterValue( } } - private static class LimitJdbcParameter extends AbstractJdbcParameter { + private static final class LimitJdbcParameter extends AbstractJdbcParameter { public LimitJdbcParameter(BasicType type) { super(type); From e95cd0086b0414e7ff26d8baa06ff00f8e061a57 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:38:50 -0400 Subject: [PATCH 39/50] Update src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java Co-authored-by: Valentin Kovalenko --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 414be3e1..2a08bc35 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -1006,7 +1006,7 @@ private static BsonValue toBsonValue(Object value) { private static final class OffsetJdbcParameter extends AbstractJdbcParameter { - public OffsetJdbcParameter(BasicType type) { + OffsetJdbcParameter(BasicType type) { super(type); } From e2fb34f3b094ab6ac64d18d79d58958a9e5fea59 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:39:02 -0400 Subject: [PATCH 40/50] Update src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java Co-authored-by: Valentin Kovalenko --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 2a08bc35..6b6ddc21 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -1030,7 +1030,7 @@ public void bindParameterValue( private static final class LimitJdbcParameter extends AbstractJdbcParameter { - public LimitJdbcParameter(BasicType type) { + LimitJdbcParameter(BasicType type) { super(type); } From 01717ead0cfc894a43ababcee21c0aef316c8434 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:42:50 -0400 Subject: [PATCH 41/50] remove final usage on local variables --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 987d627e..855eaf8d 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -400,8 +400,8 @@ public void visitOffsetFetchClause(QueryPart queryPart) { private List createSkipLimitStages(QueryPart queryPart) { var skipLimitStages = new ArrayList(2); - final Expression skipExpression; - final Expression limitExpression; + Expression skipExpression; + Expression limitExpression; if (queryPart.isRoot() && queryOptionsLimit != null && !queryOptionsLimit.isEmpty()) { var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); if (queryOptionsLimit.getFirstRow() != null) { From f863533fcef4835341245cca22c7c2e4db6c16eb Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:44:33 -0400 Subject: [PATCH 42/50] remove capacity spec in new ArrayList(3) in AbstractMqlTranslator --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 855eaf8d..ef0ecab6 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -360,7 +360,7 @@ public void visitQuerySpec(QuerySpec querySpec) { var collection = acceptAndYield(querySpec.getFromClause(), COLLECTION_NAME); - var stages = new ArrayList(3); + var stages = new ArrayList(); createMatchStage(querySpec).ifPresent(stages::add); createSortStage(querySpec).ifPresent(stages::add); From 7aa5ae837a54d161a23317d72d9e75631124cb0f Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 10:48:52 -0400 Subject: [PATCH 43/50] add comments to createSkipLimitStages() in AbstractMqlTranslator --- .../internal/translate/AbstractMqlTranslator.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index ef0ecab6..37c03500 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -403,6 +403,14 @@ private List createSkipLimitStages(QueryPart queryPart) { Expression skipExpression; Expression limitExpression; if (queryPart.isRoot() && queryOptionsLimit != null && !queryOptionsLimit.isEmpty()) { + // We check if `limits.getFirstRow`/`getMaxRows` is set,Add commentMore actions + // but ignore the actual values when creating `OffsetJdbcParameter`/`LimitJdbcParameter`. + // Hibernate ORM reuses the translation result for the same HQL/SQL queries + // with different values passed to `setFirstResult`/`setMaxResults`. Therefore, we cannot include the + // values available when translating in the translation result. The only thing we pay attention to is + // whether they are specified or not, because the translation results corresponding to + // `setFirstResult`/`setMaxResults` being present + // must be different from those with the limits being absent. Hibernate ORM also caches them separately. var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); if (queryOptionsLimit.getFirstRow() != null) { queryOptionsOffsetParameter = new OffsetJdbcParameter(basicIntegerType); From e6cdf557b326c0fbc4b8a461fd00f66afb2f3cd7 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 11:25:39 -0400 Subject: [PATCH 44/50] fix broken LimitOffsetFetchClauseIntegrationTests.QueryPlanCacheTests#testCacheInvalidatedDueToQueryOptionsRemoved --- ...imitOffsetFetchClauseIntegrationTests.java | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) 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 b8b9e7ee..9c288e3b 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -535,7 +535,7 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS expectedMqlTemplate, (isFirstResultSet ? "{\"$skip\": 5}," : ""), (isMaxResultsSet ? "{\"$limit\": 10}," : ""))); - var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCount(); assertThat(initialSelectTranslatingCount).isPositive(); @@ -547,7 +547,7 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS expectedMqlTemplate, (isFirstResultSet ? "{\"$skip\": 3}," : ""), (isMaxResultsSet ? "{\"$limit\": 6}," : ""))); - assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount); }); } @@ -556,17 +556,16 @@ void testQueryOptionsLimitCached(boolean isFirstResultSet, boolean isMaxResultsS void testCacheInvalidatedDueToQueryOptionsAdded() { getSessionFactoryScope().inTransaction(session -> { setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); - var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); - + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCount(); assertThat(initialSelectTranslatingCount).isPositive(); setQueryOptionsAndQuery(session, 1, null, format(expectedMqlTemplate, "{\"$skip\": 1},", "")); - assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 1); setQueryOptionsAndQuery( session, 1, 5, format(expectedMqlTemplate, "{\"$skip\": 1},", "{\"$limit\": 5},")); - assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) + assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 2); }); } @@ -574,32 +573,18 @@ void testCacheInvalidatedDueToQueryOptionsAdded() { @Test void testCacheInvalidatedDueToQueryOptionsRemoved() { getSessionFactoryScope().inTransaction(session -> { - setQueryOptionsAndQuery(session, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); - - var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); - + setQueryOptionsAndQuery( + session, 10, 5, format(expectedMqlTemplate, "{\"$skip\": 10},", "{\"$limit\": 5},")); + var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCount(); assertThat(initialSelectTranslatingCount).isPositive(); - setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); - - assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) + setQueryOptionsAndQuery(session, null, 5, format(expectedMqlTemplate, "", "{\"$limit\": 5},")); + assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) .isEqualTo(initialSelectTranslatingCount + 1); - }); - } - - @Test - void testCacheInvalidatedDueToQueryOptionsChanged() { - getSessionFactoryScope().inTransaction(session -> { - setQueryOptionsAndQuery(session, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); - - var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); - - assertThat(initialSelectTranslatingCount).isPositive(); - setQueryOptionsAndQuery(session, null, 20, format(expectedMqlTemplate, "", "{\"$limit\": 20},")); - - assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) - .isEqualTo(initialSelectTranslatingCount + 1); + setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); + assertThat(translatingCacheTestingDialect.getSelectTranslatingCount()) + .isEqualTo(initialSelectTranslatingCount + 2); }); } @@ -661,7 +646,7 @@ public SqlAstTranslator buildModelMutationT }; } - public int getSelectTranslatingCounter() { + public int getSelectTranslatingCount() { return selectTranslatingCounter.get(); } } From 4df77c88d26b104189660f8ffd0b176a92c6fff9 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 14:27:27 -0400 Subject: [PATCH 45/50] improve code quality by grouping related stuff together per Valentin's suggestion --- .../translate/AbstractMqlTranslator.java | 153 +++++++++++------- .../translate/AstVisitorValueDescriptor.java | 10 +- .../translate/ModelMutationMqlTranslator.java | 42 ++++- .../translate/SelectMqlTranslator.java | 74 ++++++--- .../AstVisitorValueDescriptorTests.java | 2 +- .../translate/AstVisitorValueHolderTests.java | 11 +- .../translate/SelectMqlTranslatorTests.java | 131 ++++++++------- 7 files changed, 257 insertions(+), 166 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 37c03500..0c9fc738 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -17,16 +17,17 @@ package com.mongodb.hibernate.internal.translate; import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; +import static com.mongodb.hibernate.internal.MongoAssertions.assertNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; import static com.mongodb.hibernate.internal.MongoAssertions.fail; import static com.mongodb.hibernate.internal.MongoConstants.EXTENDED_JSON_WRITER_SETTINGS; import static com.mongodb.hibernate.internal.MongoConstants.MONGO_DBMS_NAME; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_NAME; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FIELD_PATH; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MUTATION_RESULT; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SELECT_RESULT; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SORT_FIELDS; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.TUPLE; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.VALUE; @@ -54,7 +55,6 @@ import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; import com.mongodb.hibernate.internal.translate.mongoast.AstNode; import com.mongodb.hibernate.internal.translate.mongoast.AstParameterMarker; -import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.AstDeleteCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.AstInsertCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.AstUpdateCommand; @@ -200,11 +200,7 @@ abstract class AbstractMqlTranslator implements SqlAstT private final Set affectedTableNames = new HashSet<>(); - @Nullable Limit queryOptionsLimit; - - @Nullable JdbcParameter queryOptionsOffsetParameter; - - @Nullable JdbcParameter queryOptionsLimitParameter; + private @Nullable QueryOptionsLimit queryOptionsLimit; AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; @@ -244,12 +240,8 @@ public Set getAffectedTableNames() { return affectedTableNames; } - List getParameterBinders() { - return parameterBinders; - } - @SuppressWarnings("overloads") - R acceptAndYield(Statement statement, AstVisitorValueDescriptor resultDescriptor) { + R acceptAndYield(Statement statement, AstVisitorValueDescriptor resultDescriptor) { return astVisitorValueHolder.execute(resultDescriptor, () -> statement.accept(this)); } @@ -274,8 +266,11 @@ public void visitStandardTableInsert(TableInsertStandard tableInsert) { astElements.add(new AstElement(fieldName, fieldValue)); } astVisitorValueHolder.yield( - COLLECTION_MUTATION, - new AstInsertCommand(tableInsert.getMutatingTable().getTableName(), new AstDocument(astElements))); + MUTATION_RESULT, + ModelMutationMqlTranslator.Result.create( + new AstInsertCommand( + tableInsert.getMutatingTable().getTableName(), new AstDocument(astElements)), + parameterBinders)); } @Override @@ -293,8 +288,10 @@ public void visitStandardTableDelete(TableDeleteStandard tableDelete) { } var keyFilter = getKeyFilter(tableDelete); astVisitorValueHolder.yield( - COLLECTION_MUTATION, - new AstDeleteCommand(tableDelete.getMutatingTable().getTableName(), keyFilter)); + MUTATION_RESULT, + ModelMutationMqlTranslator.Result.create( + new AstDeleteCommand(tableDelete.getMutatingTable().getTableName(), keyFilter), + parameterBinders)); } @Override @@ -313,8 +310,10 @@ public void visitStandardTableUpdate(TableUpdateStandard tableUpdate) { updates.add(new AstFieldUpdate(fieldName, fieldValue)); } astVisitorValueHolder.yield( - COLLECTION_MUTATION, - new AstUpdateCommand(tableUpdate.getMutatingTable().getTableName(), keyFilter, updates)); + MUTATION_RESULT, + ModelMutationMqlTranslator.Result.create( + new AstUpdateCommand(tableUpdate.getMutatingTable().getTableName(), keyFilter, updates), + parameterBinders)); } private AstFilter getKeyFilter(AbstractRestrictedTableMutation tableMutation) { @@ -365,10 +364,20 @@ public void visitQuerySpec(QuerySpec querySpec) { createMatchStage(querySpec).ifPresent(stages::add); createSortStage(querySpec).ifPresent(stages::add); - stages.addAll(createSkipLimitStages(querySpec)); + var offsetFetchStagesAndJdbcParameters = + assertNotNull(queryOptionsLimit).createStagesAndJdbcParameters(querySpec); + stages.addAll(offsetFetchStagesAndJdbcParameters.stages()); + stages.add(createProjectStage(querySpec.getSelectClause())); - astVisitorValueHolder.yield(COLLECTION_AGGREGATE, new AstAggregateCommand(collection, stages)); + astVisitorValueHolder.yield( + SELECT_RESULT, + new SelectMqlTranslator.Result( + new AstAggregateCommand(collection, stages), + parameterBinders, + affectedTableNames, + offsetFetchStagesAndJdbcParameters.offset(), + offsetFetchStagesAndJdbcParameters.limit())); } private Optional createMatchStage(QuerySpec querySpec) { @@ -395,48 +404,68 @@ private Optional createSortStage(QuerySpec querySpec) { @Override public void visitOffsetFetchClause(QueryPart queryPart) { - fail("There is no code in Hibernate ORM that calls this method"); - } - - private List createSkipLimitStages(QueryPart queryPart) { - var skipLimitStages = new ArrayList(2); - Expression skipExpression; - Expression limitExpression; - if (queryPart.isRoot() && queryOptionsLimit != null && !queryOptionsLimit.isEmpty()) { - // We check if `limits.getFirstRow`/`getMaxRows` is set,Add commentMore actions - // but ignore the actual values when creating `OffsetJdbcParameter`/`LimitJdbcParameter`. - // Hibernate ORM reuses the translation result for the same HQL/SQL queries - // with different values passed to `setFirstResult`/`setMaxResults`. Therefore, we cannot include the - // values available when translating in the translation result. The only thing we pay attention to is - // whether they are specified or not, because the translation results corresponding to - // `setFirstResult`/`setMaxResults` being present - // must be different from those with the limits being absent. Hibernate ORM also caches them separately. - var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); - if (queryOptionsLimit.getFirstRow() != null) { - queryOptionsOffsetParameter = new OffsetJdbcParameter(basicIntegerType); + fail(); + } + + private final class QueryOptionsLimit { + private final @Nullable Limit limit; + + QueryOptionsLimit(@Nullable Limit limit) { + this.limit = limit; + } + + StagesAndJdbcParameters createStagesAndJdbcParameters(QueryPart queryPart) { + Expression skipExpression; + Expression limitExpression; + JdbcParameter offsetParameter = null; + JdbcParameter limitParameter = null; + if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { + // We check if limit.getFirstRow/getMaxRows is set, + // but ignore the actual values when creating OffsetJdbcParameter/LimitJdbcParameter. + // Hibernate ORM reuses the translation result for the same HQL/SQL queries + // with different values passed to setFirstResult/setMaxResults. Therefore, we cannot include the + // values available when translating in the translation result. The only thing we pay attention to is + // whether they are specified or not, because the translation results corresponding to + // setFirstResult/setMaxResults being present + // must be different from those with the limits being absent. Hibernate ORM also caches them separately. + var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); + if (limit.getFirstRow() != null) { + offsetParameter = new OffsetJdbcParameter(basicIntegerType); + } + if (limit.getMaxRows() != null) { + limitParameter = new LimitJdbcParameter(basicIntegerType); + } + skipExpression = offsetParameter; + limitExpression = limitParameter; + } else { + if (queryPart.getFetchClauseType() != ROWS_ONLY) { + throw new FeatureNotSupportedException(format( + "%s does not support '%s' fetch clause type", + MONGO_DBMS_NAME, queryPart.getFetchClauseType())); + } + skipExpression = queryPart.getOffsetClauseExpression(); + limitExpression = queryPart.getFetchClauseExpression(); } - if (queryOptionsLimit.getMaxRows() != null) { - queryOptionsLimitParameter = new LimitJdbcParameter(basicIntegerType); + var pipelineStages = new ArrayList(); + if (skipExpression != null) { + var skipValue = acceptAndYield(skipExpression, VALUE); + pipelineStages.add(new AstSkipStage(skipValue)); } - skipExpression = queryOptionsOffsetParameter; - limitExpression = queryOptionsLimitParameter; - } else { - if (queryPart.getFetchClauseType() != ROWS_ONLY) { - throw new FeatureNotSupportedException(format( - "%s does not support '%s' fetch clause type", MONGO_DBMS_NAME, queryPart.getFetchClauseType())); + if (limitExpression != null) { + var limitValue = acceptAndYield(limitExpression, VALUE); + pipelineStages.add(new AstLimitStage(limitValue)); } - skipExpression = queryPart.getOffsetClauseExpression(); - limitExpression = queryPart.getFetchClauseExpression(); + return new StagesAndJdbcParameters(pipelineStages, offsetParameter, limitParameter); } - if (skipExpression != null) { - var skipValue = acceptAndYield(skipExpression, VALUE); - skipLimitStages.add(new AstSkipStage(skipValue)); - } - if (limitExpression != null) { - var limitValue = acceptAndYield(limitExpression, VALUE); - skipLimitStages.add(new AstLimitStage(limitValue)); - } - return skipLimitStages; + + record StagesAndJdbcParameters( + List stages, @Nullable JdbcParameter offset, @Nullable JdbcParameter limit) {} + } + + void applyQueryOptions(QueryOptions queryOptions) { + checkQueryOptionsSupportability(queryOptions); + assertNull(queryOptionsLimit); + queryOptionsLimit = new QueryOptionsLimit(queryOptions.getLimit()); } private AstProjectStage createProjectStage(SelectClause selectClause) { @@ -927,7 +956,7 @@ static void checkJdbcParameterBindingsSupportability(@Nullable JdbcParameterBind } } - static void checkQueryOptionsSupportability(QueryOptions queryOptions) { + private static void checkQueryOptionsSupportability(QueryOptions queryOptions) { if (queryOptions.getTimeout() != null) { throw new FeatureNotSupportedException("'timeout' inQueryOptions not supported"); } @@ -1012,7 +1041,7 @@ private static BsonValue toBsonValue(Object value) { private static final class OffsetJdbcParameter extends AbstractJdbcParameter { - OffsetJdbcParameter(BasicType type) { + private OffsetJdbcParameter(BasicType type) { super(type); } @@ -1036,7 +1065,7 @@ public void bindParameterValue( private static final class LimitJdbcParameter extends AbstractJdbcParameter { - LimitJdbcParameter(BasicType type) { + private LimitJdbcParameter(BasicType type) { super(type); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java index 7a193a59..ad844f63 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -20,7 +20,6 @@ import static com.mongodb.hibernate.internal.MongoAssertions.fail; import com.mongodb.hibernate.internal.translate.mongoast.AstValue; -import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstSortField; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter; @@ -34,12 +33,15 @@ @SuppressWarnings("UnusedTypeParameter") final class AstVisitorValueDescriptor { - static final AstVisitorValueDescriptor COLLECTION_MUTATION = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor COLLECTION_AGGREGATE = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor MUTATION_RESULT = + new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor SELECT_RESULT = + new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor COLLECTION_NAME = new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor FIELD_PATH = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor VALUE = new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor> PROJECT_STAGE_SPECIFICATIONS = new AstVisitorValueDescriptor<>(); @@ -49,8 +51,6 @@ final class AstVisitorValueDescriptor { static final AstVisitorValueDescriptor> TUPLE = new AstVisitorValueDescriptor<>(); - static final AstVisitorValueDescriptor VALUE = new AstVisitorValueDescriptor<>(); - private static final Map, String> CONSTANT_TOSTRING_CONTENT_MAP; static { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java index b940323d..90b75c40 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java @@ -16,11 +16,16 @@ package com.mongodb.hibernate.internal.translate; +import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertNull; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MUTATION_RESULT; +import static java.util.Collections.emptyList; +import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; +import java.util.List; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.model.ast.TableMutation; import org.hibernate.sql.model.internal.TableUpdateNoSet; @@ -39,15 +44,38 @@ final class ModelMutationMqlTranslator extends @Override public O translate(@Nullable JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { assertNull(jdbcParameterBindings); - checkQueryOptionsSupportability(queryOptions); + applyQueryOptions(queryOptions); - String mql; + Result result; if ((TableMutation) tableMutation instanceof TableUpdateNoSet) { - mql = ""; + result = Result.empty(); } else { - var mutationCommand = acceptAndYield(tableMutation, COLLECTION_MUTATION); - mql = renderMongoAstNode(mutationCommand); + result = acceptAndYield(tableMutation, MUTATION_RESULT); + } + return result.createJdbcMutationOperation(tableMutation); + } + + static final class Result { + private final @Nullable AstCommand command; + + private final List parameterBinders; + + private Result(@Nullable AstCommand command, List parameterBinders) { + this.command = command; + this.parameterBinders = parameterBinders; + } + + static Result create(AstCommand command, List parameterBinders) { + return new Result(assertNotNull(command), parameterBinders); + } + + private static Result empty() { + return new Result(null, emptyList()); + } + + private O createJdbcMutationOperation(TableMutation tableMutation) { + var mql = command == null ? "" : renderMongoAstNode(command); + return tableMutation.createMutationOperation(mql, parameterBinders); } - return tableMutation.createMutationOperation(mql, getParameterBinders()); } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index 1f99c3d4..2e6981b6 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -16,17 +16,22 @@ package com.mongodb.hibernate.internal.translate; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SELECT_RESULT; import static java.lang.Integer.MAX_VALUE; import static java.util.Collections.emptyMap; import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; import static org.hibernate.sql.exec.spi.JdbcLockStrategy.NONE; +import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; +import java.util.List; +import java.util.Set; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; import org.jspecify.annotations.Nullable; @@ -34,13 +39,10 @@ final class SelectMqlTranslator extends AbstractMqlTranslator { private final SelectStatement selectStatement; - private final JdbcValuesMappingProducerProvider jdbcValuesMappingProducerProvider; SelectMqlTranslator(SessionFactoryImplementor sessionFactory, SelectStatement selectStatement) { super(sessionFactory); this.selectStatement = selectStatement; - jdbcValuesMappingProducerProvider = - sessionFactory.getServiceRegistry().requireService(JdbcValuesMappingProducerProvider.class); } @Override @@ -50,29 +52,51 @@ public JdbcOperationQuerySelect translate( logSqlAst(selectStatement); checkJdbcParameterBindingsSupportability(jdbcParameterBindings); - checkQueryOptionsSupportability(queryOptions); + applyQueryOptions(queryOptions); - var limit = queryOptions.getLimit(); - if (limit != null) { - queryOptionsLimit = limit.makeCopy(); - } + var result = acceptAndYield((Statement) selectStatement, SELECT_RESULT); + return result.createJdbcOperationQuerySelect(selectStatement, getSessionFactory()); + } - var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE); - var jdbcValuesMappingProducer = - jdbcValuesMappingProducerProvider.buildMappingProducer(selectStatement, getSessionFactory()); + static final class Result { + private final AstCommand command; + private final List parameterBinders; + private final Set affectedTableNames; + private final @Nullable JdbcParameter offsetParameter; + private final @Nullable JdbcParameter limitParameter; - return new JdbcOperationQuerySelect( - renderMongoAstNode(aggregateCommand), - getParameterBinders(), - jdbcValuesMappingProducer, - getAffectedTableNames(), - 0, - MAX_VALUE, - emptyMap(), - NONE, - // The following parameters are provided for query plan cache purposes. - // Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. - queryOptionsOffsetParameter, - queryOptionsLimitParameter); + Result( + AstCommand command, + List parameterBinders, + Set affectedTableNames, + @Nullable JdbcParameter offsetParameter, + @Nullable JdbcParameter limitParameter) { + this.command = command; + this.parameterBinders = parameterBinders; + this.affectedTableNames = affectedTableNames; + this.offsetParameter = offsetParameter; + this.limitParameter = limitParameter; + } + + private JdbcOperationQuerySelect createJdbcOperationQuerySelect( + SelectStatement selectStatement, SessionFactoryImplementor sessionFactory) { + var jdbcValuesMappingProducerProvider = + sessionFactory.getServiceRegistry().requireService(JdbcValuesMappingProducerProvider.class); + var jdbcValuesMappingProducer = + jdbcValuesMappingProducerProvider.buildMappingProducer(selectStatement, sessionFactory); + return new JdbcOperationQuerySelect( + renderMongoAstNode(command), + parameterBinders, + jdbcValuesMappingProducer, + affectedTableNames, + 0, + MAX_VALUE, + emptyMap(), + NONE, + // The following parameters are provided for query plan cache purposes. + // Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. + offsetParameter, + limitParameter); + } } } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptorTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptorTests.java index 011ced34..f75244fe 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptorTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptorTests.java @@ -24,6 +24,6 @@ class AstVisitorValueDescriptorTests { @Test void testToString() { - assertEquals("COLLECTION_MUTATION", AstVisitorValueDescriptor.COLLECTION_MUTATION.toString()); + assertEquals("MUTATION_RESULT", AstVisitorValueDescriptor.MUTATION_RESULT.toString()); } } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java index 2f4d09aa..c9865e63 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java @@ -16,8 +16,9 @@ package com.mongodb.hibernate.internal.translate; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_MUTATION; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MUTATION_RESULT; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.VALUE; +import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -62,10 +63,12 @@ void testRecursiveUsage() { var fieldValue = astVisitorValueHolder.execute(VALUE, fieldValueYielder); AstElement astElement = new AstElement("province", fieldValue); astVisitorValueHolder.yield( - COLLECTION_MUTATION, new AstInsertCommand("city", new AstDocument(List.of(astElement)))); + MUTATION_RESULT, + ModelMutationMqlTranslator.Result.create( + new AstInsertCommand("city", new AstDocument(List.of(astElement))), emptyList())); }; - astVisitorValueHolder.execute(COLLECTION_MUTATION, tableInserter); + astVisitorValueHolder.execute(MUTATION_RESULT, tableInserter); } @Test @@ -87,7 +90,7 @@ void testHolderExpectingDifferentDescriptor() { Runnable valueYielder = () -> astVisitorValueHolder.yield(VALUE, new AstLiteralValue(new BsonString("some_value"))); - assertThrows(Error.class, () -> astVisitorValueHolder.execute(COLLECTION_MUTATION, valueYielder)); + assertThrows(Error.class, () -> astVisitorValueHolder.execute(MUTATION_RESULT, valueYielder)); } @Test diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java index 3e70c220..2e6981b6 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java @@ -16,80 +16,87 @@ package com.mongodb.hibernate.internal.translate; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SELECT_RESULT; +import static java.lang.Integer.MAX_VALUE; +import static java.util.Collections.emptyMap; +import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; +import static org.hibernate.sql.exec.spi.JdbcLockStrategy.NONE; -import com.mongodb.hibernate.internal.extension.service.StandardServiceRegistryScopedState; +import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; +import java.util.List; +import java.util.Set; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.metamodel.mapping.SelectableMapping; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptions; -import org.hibernate.service.spi.ServiceRegistryImplementor; -import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.spi.SqlAliasBaseImpl; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.StandardTableGroup; -import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.results.internal.SqlSelectionImpl; +import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockMakers; -import org.mockito.junit.jupiter.MockitoExtension; +import org.jspecify.annotations.Nullable; -@ExtendWith(MockitoExtension.class) -class SelectMqlTranslatorTests { +final class SelectMqlTranslator extends AbstractMqlTranslator { - @Test - void testAffectedTableNames( - @Mock EntityPersister entityPersister, - @Mock(mockMaker = MockMakers.PROXY) SessionFactoryImplementor sessionFactory, - @Mock JdbcValuesMappingProducerProvider jdbcValuesMappingProducerProvider, - @Mock(mockMaker = MockMakers.PROXY) ServiceRegistryImplementor serviceRegistry, - @Mock StandardServiceRegistryScopedState standardServiceRegistryScopedState, - @Mock SelectableMapping selectableMapping) { + private final SelectStatement selectStatement; - var tableName = "books"; - SelectStatement selectFromTableName; - { // prepare `selectFromTableName` - doReturn(new String[] {tableName}).when(entityPersister).getQuerySpaces(); + SelectMqlTranslator(SessionFactoryImplementor sessionFactory, SelectStatement selectStatement) { + super(sessionFactory); + this.selectStatement = selectStatement; + } - var namedTableReference = new NamedTableReference(tableName, "b1_0"); + @Override + public JdbcOperationQuerySelect translate( + @Nullable JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { - var querySpec = new QuerySpec(true); - var tableGroup = new StandardTableGroup( - false, - new NavigablePath("Book"), - entityPersister, - null, - namedTableReference, - new SqlAliasBaseImpl("b1"), - sessionFactory); - querySpec.getFromClause().addRoot(tableGroup); - querySpec - .getSelectClause() - .addSqlSelection(new SqlSelectionImpl( - new ColumnReference(tableGroup.getPrimaryTableReference(), selectableMapping))); - selectFromTableName = new SelectStatement(querySpec); - } - { // prepare `sessionFactory` - doReturn(serviceRegistry).when(sessionFactory).getServiceRegistry(); - doReturn(jdbcValuesMappingProducerProvider) - .when(serviceRegistry) - .requireService(eq(JdbcValuesMappingProducerProvider.class)); - doReturn(standardServiceRegistryScopedState) - .when(serviceRegistry) - .requireService(eq(StandardServiceRegistryScopedState.class)); - } + logSqlAst(selectStatement); + + checkJdbcParameterBindingsSupportability(jdbcParameterBindings); + applyQueryOptions(queryOptions); - var translator = new SelectMqlTranslator(sessionFactory, selectFromTableName); + var result = acceptAndYield((Statement) selectStatement, SELECT_RESULT); + return result.createJdbcOperationQuerySelect(selectStatement, getSessionFactory()); + } + + static final class Result { + private final AstCommand command; + private final List parameterBinders; + private final Set affectedTableNames; + private final @Nullable JdbcParameter offsetParameter; + private final @Nullable JdbcParameter limitParameter; - translator.translate(null, QueryOptions.NONE); + Result( + AstCommand command, + List parameterBinders, + Set affectedTableNames, + @Nullable JdbcParameter offsetParameter, + @Nullable JdbcParameter limitParameter) { + this.command = command; + this.parameterBinders = parameterBinders; + this.affectedTableNames = affectedTableNames; + this.offsetParameter = offsetParameter; + this.limitParameter = limitParameter; + } - assertThat(translator.getAffectedTableNames()).containsExactly(tableName); + private JdbcOperationQuerySelect createJdbcOperationQuerySelect( + SelectStatement selectStatement, SessionFactoryImplementor sessionFactory) { + var jdbcValuesMappingProducerProvider = + sessionFactory.getServiceRegistry().requireService(JdbcValuesMappingProducerProvider.class); + var jdbcValuesMappingProducer = + jdbcValuesMappingProducerProvider.buildMappingProducer(selectStatement, sessionFactory); + return new JdbcOperationQuerySelect( + renderMongoAstNode(command), + parameterBinders, + jdbcValuesMappingProducer, + affectedTableNames, + 0, + MAX_VALUE, + emptyMap(), + NONE, + // The following parameters are provided for query plan cache purposes. + // Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. + offsetParameter, + limitParameter); + } } } From 5528f8620999bb51c0e4415237c3b46941a3d75c Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 22:25:17 -0400 Subject: [PATCH 46/50] throw fail() in AbstractMqlTranslator#getAffectedTableNames() --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 0c9fc738..f2eac67d 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -237,7 +237,7 @@ public Stack getCurrentClauseStack() { @Override public Set getAffectedTableNames() { - return affectedTableNames; + throw fail(); } @SuppressWarnings("overloads") From c67789775969f87f19f154d060d2af6b0283f786 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 9 Jun 2025 22:47:08 -0400 Subject: [PATCH 47/50] cosmetic improvements (renaming, etc.) --- .../translate/AbstractMqlTranslator.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index f2eac67d..6254f6f5 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -364,9 +364,9 @@ public void visitQuerySpec(QuerySpec querySpec) { createMatchStage(querySpec).ifPresent(stages::add); createSortStage(querySpec).ifPresent(stages::add); - var offsetFetchStagesAndJdbcParameters = - assertNotNull(queryOptionsLimit).createStagesAndJdbcParameters(querySpec); - stages.addAll(offsetFetchStagesAndJdbcParameters.stages()); + var skipLimitStagesAndJdbcParams = + assertNotNull(queryOptionsLimit).createSkipLimitStagesAndJdbcParams(querySpec); + stages.addAll(skipLimitStagesAndJdbcParams.stages()); stages.add(createProjectStage(querySpec.getSelectClause())); @@ -376,8 +376,8 @@ public void visitQuerySpec(QuerySpec querySpec) { new AstAggregateCommand(collection, stages), parameterBinders, affectedTableNames, - offsetFetchStagesAndJdbcParameters.offset(), - offsetFetchStagesAndJdbcParameters.limit())); + skipLimitStagesAndJdbcParams.offset(), + skipLimitStagesAndJdbcParams.limit())); } private Optional createMatchStage(QuerySpec querySpec) { @@ -414,13 +414,13 @@ private final class QueryOptionsLimit { this.limit = limit; } - StagesAndJdbcParameters createStagesAndJdbcParameters(QueryPart queryPart) { + StagesAndJdbcParameters createSkipLimitStagesAndJdbcParams(QueryPart queryPart) { Expression skipExpression; Expression limitExpression; JdbcParameter offsetParameter = null; JdbcParameter limitParameter = null; if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { - // We check if limit.getFirstRow/getMaxRows is set, + // We check if limit's firstRow/maxRows is set, // but ignore the actual values when creating OffsetJdbcParameter/LimitJdbcParameter. // Hibernate ORM reuses the translation result for the same HQL/SQL queries // with different values passed to setFirstResult/setMaxResults. Therefore, we cannot include the @@ -446,16 +446,16 @@ StagesAndJdbcParameters createStagesAndJdbcParameters(QueryPart queryPart) { skipExpression = queryPart.getOffsetClauseExpression(); limitExpression = queryPart.getFetchClauseExpression(); } - var pipelineStages = new ArrayList(); + var skipAndLimitStages = new ArrayList(); if (skipExpression != null) { var skipValue = acceptAndYield(skipExpression, VALUE); - pipelineStages.add(new AstSkipStage(skipValue)); + skipAndLimitStages.add(new AstSkipStage(skipValue)); } if (limitExpression != null) { var limitValue = acceptAndYield(limitExpression, VALUE); - pipelineStages.add(new AstLimitStage(limitValue)); + skipAndLimitStages.add(new AstLimitStage(limitValue)); } - return new StagesAndJdbcParameters(pipelineStages, offsetParameter, limitParameter); + return new StagesAndJdbcParameters(skipAndLimitStages, offsetParameter, limitParameter); } record StagesAndJdbcParameters( @@ -1041,7 +1041,7 @@ private static BsonValue toBsonValue(Object value) { private static final class OffsetJdbcParameter extends AbstractJdbcParameter { - private OffsetJdbcParameter(BasicType type) { + OffsetJdbcParameter(BasicType type) { super(type); } @@ -1065,7 +1065,7 @@ public void bindParameterValue( private static final class LimitJdbcParameter extends AbstractJdbcParameter { - private LimitJdbcParameter(BasicType type) { + LimitJdbcParameter(BasicType type) { super(type); } From cf9937f17b592181545da846d52997a2ce0e3507 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 23 Jun 2025 07:50:23 -0400 Subject: [PATCH 48/50] further code review comments resolving --- .../translate/AbstractMqlTranslator.java | 4 +- .../translate/SelectMqlTranslatorTests.java | 131 +++++++++--------- 2 files changed, 64 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 6254f6f5..5d4092b5 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -237,7 +237,7 @@ public Stack getCurrentClauseStack() { @Override public Set getAffectedTableNames() { - throw fail(); + return affectedTableNames; } @SuppressWarnings("overloads") @@ -420,6 +420,7 @@ StagesAndJdbcParameters createSkipLimitStagesAndJdbcParams(QueryPart queryPart) JdbcParameter offsetParameter = null; JdbcParameter limitParameter = null; if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { + var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); // We check if limit's firstRow/maxRows is set, // but ignore the actual values when creating OffsetJdbcParameter/LimitJdbcParameter. // Hibernate ORM reuses the translation result for the same HQL/SQL queries @@ -428,7 +429,6 @@ StagesAndJdbcParameters createSkipLimitStagesAndJdbcParams(QueryPart queryPart) // whether they are specified or not, because the translation results corresponding to // setFirstResult/setMaxResults being present // must be different from those with the limits being absent. Hibernate ORM also caches them separately. - var basicIntegerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); if (limit.getFirstRow() != null) { offsetParameter = new OffsetJdbcParameter(basicIntegerType); } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java index 2e6981b6..3e70c220 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java @@ -16,87 +16,80 @@ package com.mongodb.hibernate.internal.translate; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.SELECT_RESULT; -import static java.lang.Integer.MAX_VALUE; -import static java.util.Collections.emptyMap; -import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; -import static org.hibernate.sql.exec.spi.JdbcLockStrategy.NONE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; -import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; -import java.util.List; -import java.util.Set; +import com.mongodb.hibernate.internal.extension.service.StandardServiceRegistryScopedState; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.QueryOptions; -import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.spi.SqlAliasBaseImpl; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; -import org.hibernate.sql.exec.spi.JdbcParameterBinder; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; -import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockMakers; +import org.mockito.junit.jupiter.MockitoExtension; -final class SelectMqlTranslator extends AbstractMqlTranslator { +@ExtendWith(MockitoExtension.class) +class SelectMqlTranslatorTests { - private final SelectStatement selectStatement; + @Test + void testAffectedTableNames( + @Mock EntityPersister entityPersister, + @Mock(mockMaker = MockMakers.PROXY) SessionFactoryImplementor sessionFactory, + @Mock JdbcValuesMappingProducerProvider jdbcValuesMappingProducerProvider, + @Mock(mockMaker = MockMakers.PROXY) ServiceRegistryImplementor serviceRegistry, + @Mock StandardServiceRegistryScopedState standardServiceRegistryScopedState, + @Mock SelectableMapping selectableMapping) { - SelectMqlTranslator(SessionFactoryImplementor sessionFactory, SelectStatement selectStatement) { - super(sessionFactory); - this.selectStatement = selectStatement; - } - - @Override - public JdbcOperationQuerySelect translate( - @Nullable JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { + var tableName = "books"; + SelectStatement selectFromTableName; + { // prepare `selectFromTableName` + doReturn(new String[] {tableName}).when(entityPersister).getQuerySpaces(); - logSqlAst(selectStatement); + var namedTableReference = new NamedTableReference(tableName, "b1_0"); - checkJdbcParameterBindingsSupportability(jdbcParameterBindings); - applyQueryOptions(queryOptions); - - var result = acceptAndYield((Statement) selectStatement, SELECT_RESULT); - return result.createJdbcOperationQuerySelect(selectStatement, getSessionFactory()); - } + var querySpec = new QuerySpec(true); + var tableGroup = new StandardTableGroup( + false, + new NavigablePath("Book"), + entityPersister, + null, + namedTableReference, + new SqlAliasBaseImpl("b1"), + sessionFactory); + querySpec.getFromClause().addRoot(tableGroup); + querySpec + .getSelectClause() + .addSqlSelection(new SqlSelectionImpl( + new ColumnReference(tableGroup.getPrimaryTableReference(), selectableMapping))); + selectFromTableName = new SelectStatement(querySpec); + } + { // prepare `sessionFactory` + doReturn(serviceRegistry).when(sessionFactory).getServiceRegistry(); + doReturn(jdbcValuesMappingProducerProvider) + .when(serviceRegistry) + .requireService(eq(JdbcValuesMappingProducerProvider.class)); + doReturn(standardServiceRegistryScopedState) + .when(serviceRegistry) + .requireService(eq(StandardServiceRegistryScopedState.class)); + } - static final class Result { - private final AstCommand command; - private final List parameterBinders; - private final Set affectedTableNames; - private final @Nullable JdbcParameter offsetParameter; - private final @Nullable JdbcParameter limitParameter; + var translator = new SelectMqlTranslator(sessionFactory, selectFromTableName); - Result( - AstCommand command, - List parameterBinders, - Set affectedTableNames, - @Nullable JdbcParameter offsetParameter, - @Nullable JdbcParameter limitParameter) { - this.command = command; - this.parameterBinders = parameterBinders; - this.affectedTableNames = affectedTableNames; - this.offsetParameter = offsetParameter; - this.limitParameter = limitParameter; - } + translator.translate(null, QueryOptions.NONE); - private JdbcOperationQuerySelect createJdbcOperationQuerySelect( - SelectStatement selectStatement, SessionFactoryImplementor sessionFactory) { - var jdbcValuesMappingProducerProvider = - sessionFactory.getServiceRegistry().requireService(JdbcValuesMappingProducerProvider.class); - var jdbcValuesMappingProducer = - jdbcValuesMappingProducerProvider.buildMappingProducer(selectStatement, sessionFactory); - return new JdbcOperationQuerySelect( - renderMongoAstNode(command), - parameterBinders, - jdbcValuesMappingProducer, - affectedTableNames, - 0, - MAX_VALUE, - emptyMap(), - NONE, - // The following parameters are provided for query plan cache purposes. - // Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. - offsetParameter, - limitParameter); - } + assertThat(translator.getAffectedTableNames()).containsExactly(tableName); } } From 64b4d0e76643142da0b712c2ecb7539c58e96910 Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Mon, 23 Jun 2025 13:29:14 -0400 Subject: [PATCH 49/50] further changes as per Valentin's comments --- .../hibernate/internal/translate/AbstractMqlTranslator.java | 2 +- .../internal/translate/SelectMqlTranslatorTests.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java index 5d4092b5..4d60b1ef 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -237,7 +237,7 @@ public Stack getCurrentClauseStack() { @Override public Set getAffectedTableNames() { - return affectedTableNames; + throw fail(); } @SuppressWarnings("overloads") diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java index 3e70c220..9e99a968 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java @@ -88,8 +88,7 @@ void testAffectedTableNames( var translator = new SelectMqlTranslator(sessionFactory, selectFromTableName); - translator.translate(null, QueryOptions.NONE); - - assertThat(translator.getAffectedTableNames()).containsExactly(tableName); + assertThat(translator.translate(null, QueryOptions.NONE).getAffectedTableNames()) + .containsExactly(tableName); } } From 2a7cb9e1990aa67b2aaa374cc60c2cd169a217cf Mon Sep 17 00:00:00 2001 From: Nathan Xu Date: Wed, 2 Jul 2025 16:09:05 -0400 Subject: [PATCH 50/50] change TranslatingCacheTestingDialect visibility from public to proteced in LimitOffsetFetchClauseIntegrationTests --- .../query/select/LimitOffsetFetchClauseIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9c288e3b..e8263e4a 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -613,7 +613,7 @@ private void setQueryOptionsAndQuery( * the query plan cache is hit, not whether {@link SqlAstTranslator} is reused afterwards (e.g., incompatible * {@link org.hibernate.query.spi.QueryOptions QueryOptions}s will end up with new translator bing created). */ - public static final class TranslatingCacheTestingDialect extends Dialect { + protected static final class TranslatingCacheTestingDialect extends Dialect { private final AtomicInteger selectTranslatingCounter = new AtomicInteger(); private final Dialect delegate;