diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java new file mode 100644 index 00000000..955a6737 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java @@ -0,0 +1,283 @@ +/* + * 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; + +import static com.mongodb.hibernate.MongoTestAssertions.assertIterableEq; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hibernate.cfg.JdbcSettings.DIALECT; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.TestCommandListener; +import com.mongodb.hibernate.dialect.MongoDialect; +import com.mongodb.hibernate.junit.MongoExtension; +import java.util.Set; +import java.util.function.Consumer; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.bson.BsonDocument; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.SelectionQuery; +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.AbstractJdbcOperationQuery; +import org.hibernate.sql.exec.spi.JdbcOperation; +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.ServiceRegistry; +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.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.stubbing.Answer; + +@SessionFactory(exportSchema = false) +@ServiceRegistry( + settings = + @Setting( + name = DIALECT, + value = + "com.mongodb.hibernate.query.AbstractQueryIntegrationTests$TranslateResultAwareDialect")) +@ExtendWith(MongoExtension.class) +public abstract class AbstractQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { + + private SessionFactoryScope sessionFactoryScope; + + private TestCommandListener testCommandListener; + + protected SessionFactoryScope getSessionFactoryScope() { + return sessionFactoryScope; + } + + protected TestCommandListener getTestCommandListener() { + return testCommandListener; + } + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Override + public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope) { + this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class); + } + + protected void assertSelectionQuery( + String hql, + Class resultType, + Consumer> queryPostProcessor, + String expectedMql, + Iterable expectedResultList, + Set expectedAffectedCollections) { + assertSelectionQuery( + hql, + resultType, + queryPostProcessor, + expectedMql, + resultList -> assertIterableEq(expectedResultList, resultList), + expectedAffectedCollections); + } + + protected void assertSelectionQuery( + String hql, + Class resultType, + String expectedMql, + Iterable expectedResultList, + Set expectedAffectedCollections) { + assertSelectionQuery(hql, resultType, null, expectedMql, expectedResultList, expectedAffectedCollections); + } + + protected void assertSelectionQuery( + String hql, + Class resultType, + Consumer> queryPostProcessor, + String expectedMql, + Consumer> resultListVerifier, + Set expectedAffectedCollections) { + 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); + + assertAffectedCollections(expectedAffectedCollections); + }); + } + + protected void assertSelectionQuery( + String hql, + Class resultType, + String expectedMql, + Consumer> resultListVerifier, + Set expectedAffectedCollections) { + assertSelectionQuery(hql, resultType, null, expectedMql, resultListVerifier, expectedAffectedCollections); + } + + protected 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)); + } + + protected void assertSelectQueryFailure( + String hql, + Class resultType, + Class expectedExceptionType, + String expectedExceptionMessage, + Object... expectedExceptionMessageParameters) { + assertSelectQueryFailure( + hql, + resultType, + null, + expectedExceptionType, + expectedExceptionMessage, + expectedExceptionMessageParameters); + } + + protected void assertActualCommand(BsonDocument expectedCommand) { + var capturedCommands = testCommandListener.getStartedCommands(); + + assertThat(capturedCommands) + .singleElement() + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsAllEntriesOf(expectedCommand); + } + + protected void assertMutationQuery( + String hql, + Consumer queryPostProcessor, + int expectedMutationCount, + String expectedMql, + MongoCollection collection, + Iterable expectedDocuments, + Set expectedAffectedCollections) { + sessionFactoryScope.inTransaction(session -> { + var query = session.createMutationQuery(hql); + if (queryPostProcessor != null) { + queryPostProcessor.accept(query); + } + var mutationCount = query.executeUpdate(); + assertActualCommand(BsonDocument.parse(expectedMql)); + assertThat(mutationCount).isEqualTo(expectedMutationCount); + }); + assertThat(collection.find()).containsExactlyElementsOf(expectedDocuments); + assertAffectedCollections(expectedAffectedCollections); + } + + protected void assertMutationQueryFailure( + String hql, + Consumer queryPostProcessor, + Class expectedExceptionType, + String expectedExceptionMessage, + Object... expectedExceptionMessageParameters) { + sessionFactoryScope.inTransaction(session -> assertThatThrownBy(() -> { + var query = session.createMutationQuery(hql); + if (queryPostProcessor != null) { + queryPostProcessor.accept(query); + } + query.executeUpdate(); + }) + .isInstanceOf(expectedExceptionType) + .hasMessage(expectedExceptionMessage, expectedExceptionMessageParameters)); + } + + private void assertAffectedCollections(Set expectedAffectedCollections) { + assertThat(((TranslateResultAwareDialect) getSessionFactoryScope() + .getSessionFactory() + .getJdbcServices() + .getDialect()) + .capturedTranslateResult.getAffectedTableNames()) + .containsExactlyInAnyOrderElementsOf(expectedAffectedCollections); + } + + protected static final class TranslateResultAwareDialect extends Dialect { + private final Dialect delegate; + private AbstractJdbcOperationQuery capturedTranslateResult; + + public TranslateResultAwareDialect(DialectResolutionInfo info) { + super(info); + delegate = new MongoDialect(info); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new SqlAstTranslatorFactory() { + @Override + public SqlAstTranslator buildSelectTranslator( + SessionFactoryImplementor sessionFactory, SelectStatement statement) { + return createCapturingTranslator( + delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement)); + } + + @Override + public SqlAstTranslator buildMutationTranslator( + SessionFactoryImplementor sessionFactory, MutationStatement statement) { + return createCapturingTranslator( + delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement)); + } + + @Override + public SqlAstTranslator buildModelMutationTranslator( + TableMutation mutation, SessionFactoryImplementor sessionFactory) { + return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); + } + + private SqlAstTranslator createCapturingTranslator( + SqlAstTranslator originalTranslator) { + var translatorSpy = spy(originalTranslator); + doAnswer((Answer) invocation -> { + capturedTranslateResult = (AbstractJdbcOperationQuery) invocation.callRealMethod(); + return capturedTranslateResult; + }) + .when(translatorSpy) + .translate(any(), any()); + return translatorSpy; + } + }; + } + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java b/src/integrationTest/java/com/mongodb/hibernate/query/Book.java similarity index 66% rename from src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java rename to src/integrationTest/java/com/mongodb/hibernate/query/Book.java index 098b145d..e4b9b3f8 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/Book.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mongodb.hibernate.query.select; +package com.mongodb.hibernate.query; import jakarta.persistence.Entity; import jakarta.persistence.Id; @@ -22,22 +22,24 @@ import java.math.BigDecimal; @Entity(name = "Book") -@Table(name = "books") -class Book { +@Table(name = Book.COLLECTION_NAME) +public class Book { + public static final String COLLECTION_NAME = "books"; + @Id - int id; + public int id; // TODO-HIBERNATE-48 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"); + public String title = ""; + public Boolean outOfStock = false; + public Integer publishYear = 0; + public Long isbn13 = 0L; + public Double discount = 0.0; + public BigDecimal price = new BigDecimal("0.0"); - Book() {} + public Book() {} - Book(int id, String title, Integer publishYear, Boolean outOfStock) { + public Book(int id, String title, Integer publishYear, Boolean outOfStock) { this.id = id; this.title = title; this.publishYear = publishYear; diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java new file mode 100644 index 00000000..906966a5 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java @@ -0,0 +1,196 @@ +/* + * 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.mutation; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; +import java.util.List; +import java.util.Set; +import org.bson.BsonDocument; +import org.hibernate.testing.orm.junit.DomainModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = Book.class) +class DeletionIntegrationTests extends AbstractQueryIntegrationTests { + + @InjectMongoCollection(Book.COLLECTION_NAME) + private static MongoCollection mongoCollection; + + 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), + new Book(4, "The Brothers Karamazov", 1880, false), + new Book(5, "War and Peace", 2025, false)); + + @BeforeEach + void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); + } + + @Test + void testDeletionWithNonZeroMutationCount() { + assertMutationQuery( + "delete from Book where title = :title", + q -> q.setParameter("title", "War and Peace"), + 2, + """ + { + "delete": "books", + "deletes": [ + { + "limit": 0, + "q": { + "title": { + "$eq": "War and Peace" + } + } + } + ] + } + """, + mongoCollection, + List.of( + BsonDocument.parse( + """ + { + "_id": 2, + "title": "Crime and Punishment", + "outOfStock": false, + "publishYear": 1866, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 3, + "title": "Anna Karenina", + "outOfStock": false, + "publishYear": 1877, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 4, + "title": "The Brothers Karamazov", + "outOfStock": false, + "publishYear": 1880, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """)), + Set.of(Book.COLLECTION_NAME)); + } + + @Test + void testDeletionWithZeroMutationCount() { + assertMutationQuery( + "delete from Book where publishYear < :year", + q -> q.setParameter("year", 1850), + 0, + """ + { + "delete": "books", + "deletes": [ + { + "limit": 0, + "q": { + "publishYear": { + "$lt": 1850 + } + } + } + ] + } + """, + mongoCollection, + List.of( + BsonDocument.parse( + """ + { + "_id": 1, + "title": "War and Peace", + "outOfStock": true, + "publishYear": 1869, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 2, + "title": "Crime and Punishment", + "outOfStock": false, + "publishYear": 1866, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 3, + "title": "Anna Karenina", + "outOfStock": false, + "publishYear": 1877, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 4, + "title": "The Brothers Karamazov", + "outOfStock": false, + "publishYear": 1880, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 5, + "title": "War and Peace", + "outOfStock": false, + "publishYear": 2025, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """)), + Set.of(Book.COLLECTION_NAME)); + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/InsertionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/InsertionIntegrationTests.java new file mode 100644 index 00000000..8ce0a724 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/InsertionIntegrationTests.java @@ -0,0 +1,160 @@ +/* + * 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.mutation; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; +import java.util.List; +import java.util.Set; +import org.bson.BsonDocument; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.testing.orm.junit.DomainModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = Book.class) +class InsertionIntegrationTests extends AbstractQueryIntegrationTests { + + @InjectMongoCollection(Book.COLLECTION_NAME) + private static MongoCollection mongoCollection; + + @BeforeEach + void beforeEach() { + getTestCommandListener().clear(); + } + + @Test + void testInsertSingleDocument() { + assertMutationQuery( + "insert into Book (id, title, outOfStock, publishYear, isbn13, discount, price) values (1, 'Pride & Prejudice', false, 1813, 9780141439518L, 0.2D, 23.55BD)", + null, + 1, + """ + { + "insert": "books", + "documents": [ + { + "_id": 1, + "title": "Pride & Prejudice", + "outOfStock": false, + "publishYear": 1813, + "isbn13": 9780141439518, + "discount": 0.2, + "price": {"$numberDecimal": "23.55"} + } + ] + } + """, + mongoCollection, + List.of( + BsonDocument.parse( + """ + { + "_id": 1, + "title": "Pride & Prejudice", + "outOfStock": false, + "publishYear": 1813, + "isbn13": 9780141439518, + "discount": 0.2, + "price": {"$numberDecimal": "23.55"} + } + """)), + Set.of(Book.COLLECTION_NAME)); + } + + @Test + void testInsertMultipleDocuments() { + assertMutationQuery( + """ + insert into Book (id, title, outOfStock, publishYear, isbn13, discount, price) + values + (1, 'Pride & Prejudice', false, 1813, 9780141439518L, 0.2D, 23.55BD), + (2, 'War & Peace', false, 1867, 9780143039990L, 0.1D, 19.99BD) + """, + null, + 2, + """ + { + "insert": "books", + "documents": [ + { + "_id": 1, + "title": "Pride & Prejudice", + "outOfStock": false, + "publishYear": 1813, + "isbn13": 9780141439518, + "discount": 0.2, + "price": {"$numberDecimal": "23.55"} + }, + { + "_id": 2, + "title": "War & Peace", + "outOfStock": false, + "publishYear": 1867, + "isbn13": 9780143039990, + "discount": 0.1, + "price": {"$numberDecimal": "19.99"} + } + ] + } + """, + mongoCollection, + List.of( + BsonDocument.parse( + """ + { + "_id": 1, + "title": "Pride & Prejudice", + "outOfStock": false, + "publishYear": 1813, + "isbn13": 9780141439518, + "discount": 0.2, + "price": {"$numberDecimal": "23.55"} + } + """), + BsonDocument.parse( + """ + { + "_id": 2, + "title": "War & Peace", + "outOfStock": false, + "publishYear": 1867, + "isbn13": 9780143039990, + "discount": 0.1, + "price": {"$numberDecimal": "19.99"} + } + """)), + Set.of(Book.COLLECTION_NAME)); + } + + @Test + @Disabled("TODO-HIBERNATE-95 https://jira.mongodb.org/browse/HIBERNATE-95 enable this test") + void testConstraintViolationExceptionIsThrown() { + var hql = + """ + insert into Book (id, title, outOfStock, publishYear, isbn13, discount, price) + values + (:id, 'Pride & Prejudice', false, 1813, 9780141439518L, 0.2D, 23.55BD), + (:id, 'Pride & Prejudice', false, 1813, 9780141439518L, 0.2D, 23.55BD) + """; + assertMutationQueryFailure( + hql, query -> query.setParameter("id", 1), ConstraintViolationException.class, "to be decided"); + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java new file mode 100644 index 00000000..b1a4d44b --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java @@ -0,0 +1,231 @@ +/* + * 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.mutation; + +import com.mongodb.client.MongoCollection; +import com.mongodb.hibernate.junit.InjectMongoCollection; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; +import java.util.List; +import java.util.Set; +import org.bson.BsonDocument; +import org.hibernate.testing.orm.junit.DomainModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = Book.class) +class UpdatingIntegrationTests extends AbstractQueryIntegrationTests { + + @InjectMongoCollection(Book.COLLECTION_NAME) + private static MongoCollection mongoCollection; + + private static final List testingBooks = List.of( + new Book(1, "War & 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 & Peace", 2025, false)); + + @BeforeEach + protected void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); + } + + @Test + void testUpdateWithNonZeroMutationCount() { + assertMutationQuery( + "update Book set title = :newTitle, outOfStock = false where title = :oldTitle", + q -> q.setParameter("oldTitle", "War & Peace").setParameter("newTitle", "War and Peace"), + 2, + """ + { + "update": "books", + "updates": [ + { + "multi": true, + "q": { + "title": { + "$eq": "War & Peace" + } + }, + "u": { + "$set": { + "title": "War and Peace", + "outOfStock": false + } + } + } + ] + } + """, + mongoCollection, + List.of( + BsonDocument.parse( + """ + { + "_id": 1, + "title": "War and Peace", + "outOfStock": false, + "publishYear": 1869, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 2, + "title": "Crime and Punishment", + "outOfStock": false, + "publishYear": 1866, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 3, + "title": "Anna Karenina", + "outOfStock": false, + "publishYear": 1877, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 4, + "title": "The Brothers Karamazov", + "outOfStock": false, + "publishYear": 1880, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 5, + "title": "War and Peace", + "outOfStock": false, + "publishYear": 2025, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """)), + Set.of(Book.COLLECTION_NAME)); + } + + @Test + void testUpdateWithZeroMutationCount() { + assertMutationQuery( + "update Book set outOfStock = false where publishYear < :year", + q -> q.setParameter("year", 1850), + 0, + """ + { + "update": "books", + "updates": [ + { + "multi": true, + "q": { + "publishYear": { + "$lt": 1850 + } + }, + "u": { + "$set": { + "outOfStock": false + } + } + } + ] + } + """, + mongoCollection, + List.of( + BsonDocument.parse( + """ + { + "_id": 1, + "title": "War & Peace", + "outOfStock": true, + "publishYear": 1869, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 2, + "title": "Crime and Punishment", + "outOfStock": false, + "publishYear": 1866, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 3, + "title": "Anna Karenina", + "outOfStock": false, + "publishYear": 1877, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 4, + "title": "The Brothers Karamazov", + "outOfStock": false, + "publishYear": 1880, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """), + BsonDocument.parse( + """ + { + "_id": 5, + "title": "War & Peace", + "outOfStock": false, + "publishYear": 2025, + "isbn13": {"$numberLong": "0"}, + "discount": {"$numberDouble": "0"}, + "price": {"$numberDecimal": "0.0"} + } + """)), + Set.of(Book.COLLECTION_NAME)); + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java deleted file mode 100644 index 0ddd2aac..00000000 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java +++ /dev/null @@ -1,146 +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.query.select; - -import static com.mongodb.hibernate.MongoTestAssertions.assertIterableEq; -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 { - - private SessionFactoryScope sessionFactoryScope; - - private TestCommandListener testCommandListener; - - SessionFactoryScope getSessionFactoryScope() { - return sessionFactoryScope; - } - - TestCommandListener getTestCommandListener() { - return 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 -> assertIterableEq(expectedResultList, resultList)); - } - - void assertSelectionQuery(String hql, Class resultType, String expectedMql, List expectedResultList) { - assertSelectionQuery(hql, resultType, null, expectedMql, 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 assertSelectionQuery( - String hql, Class resultType, String expectedMql, Consumer> resultListVerifier) { - assertSelectionQuery(hql, resultType, null, expectedMql, resultListVerifier); - } - - 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 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(); - - assertThat(capturedCommands) - .singleElement() - .asInstanceOf(InstanceOfAssertFactories.MAP) - .containsAllEntriesOf(expectedCommand); - } -} 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 3eb51677..1e42d6cf 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java @@ -19,19 +19,22 @@ import static java.util.Collections.singletonList; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; +import java.util.Set; import org.hibernate.testing.orm.junit.DomainModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) -class BooleanExpressionWhereClauseIntegrationTests extends AbstractSelectionQueryIntegrationTests { +class BooleanExpressionWhereClauseIntegrationTests extends AbstractQueryIntegrationTests { private Book bookOutOfStock; private Book bookInStock; @BeforeEach - void beforeEach() { + protected void beforeEach() { bookOutOfStock = new Book(); bookOutOfStock.id = 1; bookOutOfStock.outOfStock = true; @@ -54,15 +57,39 @@ void testBooleanFieldPathExpression(boolean negated) { assertSelectionQuery( "from Book where" + (negated ? " not " : " ") + "outOfStock", Book.class, - "{'aggregate': 'books', 'pipeline': [{'$match': {'outOfStock': {'$eq': " - + (negated ? "false" : "true") - + "}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}", - negated ? singletonList(bookInStock) : singletonList(bookOutOfStock)); + """ + { + "aggregate": "books", + "pipeline": [ + { + "$match": { + "outOfStock": { + "$eq": %s + } + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + } + """ + .formatted(negated ? "false" : "true"), + negated ? singletonList(bookInStock) : singletonList(bookOutOfStock), + Set.of(Book.COLLECTION_NAME)); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void testNonFieldPathExpressionNotSupported(final boolean booleanLiteral) { + void testNonFieldPathExpressionNotSupported(boolean booleanLiteral) { assertSelectQueryFailure( "from Book where " + booleanLiteral, Book.class, 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 e8263e4a..ed8fd76e 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java @@ -26,8 +26,11 @@ import com.mongodb.hibernate.dialect.MongoDialect; import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.MongoConstants; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.bson.BsonDocument; import org.hibernate.Session; @@ -56,7 +59,7 @@ import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) -class LimitOffsetFetchClauseIntegrationTests extends AbstractSelectionQueryIntegrationTests { +class LimitOffsetFetchClauseIntegrationTests extends AbstractQueryIntegrationTests { private static final List testingBooks = List.of( new Book(0, "Nostromo", 1904, true), @@ -122,7 +125,8 @@ void testHqlLimitClauseOnly(boolean useLiteralParameter) { } """ .formatted(5), - getBooksByIds(0, 1, 2, 3, 4)); + getBooksByIds(0, 1, 2, 3, 4), + Set.of(Book.COLLECTION_NAME)); } @ParameterizedTest @@ -159,7 +163,8 @@ void testHqlOffsetClauseOnly(boolean useLiteralParameter) { } """ .formatted(7), - getBooksByIds(7, 8, 9)); + getBooksByIds(7, 8, 9), + Set.of(Book.COLLECTION_NAME)); } @ParameterizedTest @@ -203,7 +208,8 @@ void testHqlLimitAndOffsetClauses(boolean useLiteralParameters) { } """ .formatted(3, 2), - getBooksByIds(3, 4)); + getBooksByIds(3, 4), + Set.of(Book.COLLECTION_NAME)); } @ParameterizedTest @@ -244,7 +250,8 @@ void testHqlFetchClauseOnly(String fetchClause) { } """ .formatted(5), - getBooksByIds(0, 1, 2, 3, 4)); + getBooksByIds(0, 1, 2, 3, 4), + Set.of(Book.COLLECTION_NAME)); } } @@ -286,7 +293,8 @@ void testQueryOptionsSetFirstResultOnly() { } """ .formatted(6), - getBooksByIds(6, 7, 8, 9)); + getBooksByIds(6, 7, 8, 9), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -322,7 +330,8 @@ void testQueryOptionsSetMaxResultOnly() { } """ .formatted(3), - getBooksByIds(0, 1, 2)); + getBooksByIds(0, 1, 2), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -361,7 +370,8 @@ void testQueryOptionsSetFirstResultAndMaxResults() { } """ .formatted(2, 3), - getBooksByIds(2, 3, 4)); + getBooksByIds(2, 3, 4), + Set.of(Book.COLLECTION_NAME)); } } @@ -407,7 +417,8 @@ void testFirstResultConflictingOnly() { .setParameter("offset", 0) .setFirstResult(firstResult), expectedMqlTemplate.formatted("{\"$skip\": " + firstResult + "}"), - expectedBooks); + expectedBooks, + Set.of(Book.COLLECTION_NAME)); } @Test @@ -423,7 +434,8 @@ void testMaxResultsConflictingOnly() { .setParameter("offset", 0) .setMaxResults(maxResults), expectedMqlTemplate.formatted("{\"$limit\": " + maxResults + "}"), - expectedBooks); + expectedBooks, + Set.of(Book.COLLECTION_NAME)); } @Test @@ -442,7 +454,8 @@ void testBothFirstResultAndMaxResultsConflicting() { .setMaxResults(maxResults), expectedMqlTemplate.formatted( "{\"$skip\": " + firstResult + "}," + "{\"$limit\": " + maxResults + "}"), - expectedBooks); + expectedBooks, + Set.of(Book.COLLECTION_NAME)); } } } @@ -482,7 +495,7 @@ void testUnsupportedFetchClauseType(FetchClauseType fetchClauseType) { value = "com.mongodb.hibernate.query.select.LimitOffsetFetchClauseIntegrationTests$TranslatingCacheTestingDialect"), }) - class QueryPlanCacheTests extends AbstractSelectionQueryIntegrationTests { + class QueryPlanCacheTests extends AbstractQueryIntegrationTests { private static final String HQL = "from Book order by id"; private static final String expectedMqlTemplate = diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java index 5e454851..ab098c6c 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -16,16 +16,18 @@ package com.mongodb.hibernate.query.select; -import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThatCode; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.hibernate.query.SemanticException; import org.hibernate.testing.orm.junit.DomainModel; import org.junit.jupiter.api.BeforeEach; @@ -35,7 +37,7 @@ import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = {SimpleSelectQueryIntegrationTests.Contact.class, Book.class}) -class SimpleSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { +class SimpleSelectQueryIntegrationTests extends AbstractQueryIntegrationTests { @Nested class QueryTests { @@ -90,7 +92,8 @@ void testComparisonByEq(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 5)); + getTestingContacts(1, 5), + Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @@ -121,7 +124,8 @@ void testComparisonByNe(boolean fieldAsLhs) { } ] }""", - getTestingContacts(2, 3, 4)); + getTestingContacts(2, 3, 4), + Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @@ -152,7 +156,8 @@ void testComparisonByLt(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 3, 5)); + getTestingContacts(1, 3, 5), + Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @@ -183,7 +188,8 @@ void testComparisonByLte(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 2, 3, 5)); + getTestingContacts(1, 2, 3, 5), + Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @@ -214,7 +220,8 @@ void testComparisonByGt(boolean fieldAsLhs) { } ] }""", - getTestingContacts(2, 4, 5)); + getTestingContacts(2, 4, 5), + Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @@ -245,7 +252,8 @@ void testComparisonByGte(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 2, 4, 5)); + getTestingContacts(1, 2, 4, 5), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -284,7 +292,8 @@ void testAndFilter() { } ] }""", - getTestingContacts(2, 4)); + getTestingContacts(2, 4), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -323,7 +332,8 @@ void testOrFilter() { } ] }""", - getTestingContacts(2, 3, 4, 5)); + getTestingContacts(2, 3, 4, 5), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -365,7 +375,8 @@ void testSingleNegation() { } ] }""", - getTestingContacts(2, 4)); + getTestingContacts(2, 4), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -409,7 +420,8 @@ void testSingleNegationWithAnd() { } ] }""", - getTestingContacts(1, 2, 3, 4)); + getTestingContacts(1, 2, 3, 4), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -453,7 +465,8 @@ void testSingleNegationWithOr() { } ] }""", - getTestingContacts(3)); + getTestingContacts(3), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -508,7 +521,8 @@ void testSingleNegationWithAndOr() { } ] }""", - getTestingContacts(2, 4)); + getTestingContacts(2, 4), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -554,7 +568,8 @@ void testDoubleNegation() { } ] }""", - getTestingContacts(5)); + getTestingContacts(5), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -582,7 +597,8 @@ void testProjectWithoutAlias() { } ] }""", - List.of(new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78})); + List.of(new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78}), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -610,7 +626,8 @@ void testProjectUsingAlias() { } ] }""", - List.of(new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78})); + List.of(new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78}), + Set.of(Contact.COLLECTION_NAME)); } @Test @@ -741,7 +758,8 @@ void testBoolean() { } ] }""", - singletonList(testingBook)); + List.of(testingBook), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -773,7 +791,8 @@ void testInteger() { } ] }""", - singletonList(testingBook)); + List.of(testingBook), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -805,7 +824,8 @@ void testLong() { } ] }""", - singletonList(testingBook)); + List.of(testingBook), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -837,7 +857,8 @@ void testDouble() { } ] }""", - singletonList(testingBook)); + List.of(testingBook), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -869,7 +890,8 @@ void testString() { } ] }""", - singletonList(testingBook)); + List.of(testingBook), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -903,13 +925,17 @@ void testBigDecimal() { } ] }""", - singletonList(testingBook)); + List.of(testingBook), + Set.of(Book.COLLECTION_NAME)); } } @Entity(name = "Contact") - @Table(name = "contacts") + @Table(name = Contact.COLLECTION_NAME) static class Contact { + + static final String COLLECTION_NAME = "contacts"; + @Id int id; diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java index 19379ebb..75dfd9a1 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -25,8 +25,11 @@ import static org.hibernate.query.SortDirection.ASCENDING; import com.mongodb.hibernate.internal.FeatureNotSupportedException; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import com.mongodb.hibernate.query.Book; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.Setting; @@ -37,7 +40,7 @@ import org.junit.jupiter.params.provider.ValueSource; @DomainModel(annotatedClasses = Book.class) -class SortingSelectQueryIntegrationTests extends AbstractSelectionQueryIntegrationTests { +class SortingSelectQueryIntegrationTests extends AbstractQueryIntegrationTests { private static final List testingBooks = List.of( new Book(1, "War and Peace", 1869, true), @@ -56,7 +59,7 @@ private static List getBooksByIds(int... ids) { } @BeforeEach - void beforeEach() { + protected void beforeEach() { getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); getTestCommandListener().clear(); } @@ -91,7 +94,8 @@ void testOrderBySingleFieldWithoutTies(String sortDirection) { } """ .formatted(sortDirection.equals("ASC") ? 1 : -1), - sortDirection.equals("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), + Set.of(Book.COLLECTION_NAME)); } @ParameterizedTest @@ -132,7 +136,8 @@ void testOrderBySingleFieldWithTies(String sortDirection) { : resultList -> assertThat(resultList) .satisfiesAnyOf( list -> assertIterableEq(getBooksByIds(1, 5, 4, 2, 3), list), - list -> assertIterableEq(getBooksByIds(5, 1, 4, 2, 3), list))); + list -> assertIterableEq(getBooksByIds(5, 1, 4, 2, 3), list)), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -171,7 +176,8 @@ void testOrderByMultipleFieldsWithoutTies() { } ] }""", - getBooksByIds(3, 2, 4, 5)); + getBooksByIds(3, 2, 4, 5), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -183,7 +189,8 @@ void testOrderByMultipleFieldsWithTies() { resultList -> assertThat(resultList) .satisfiesAnyOf( list -> assertIterableEq(getBooksByIds(3, 2, 4, 1, 5), list), - list -> assertIterableEq(getBooksByIds(3, 2, 4, 5, 1), list))); + list -> assertIterableEq(getBooksByIds(3, 2, 4, 5, 1), list)), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -215,7 +222,8 @@ void testSortFieldByAlias() { 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}), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -246,13 +254,14 @@ void testSortFieldByOrdinalReference() { new Object[] {"Crime and Punishment", 1866}, new Object[] {"The Brothers Karamazov", 1880}, new Object[] {"War and Peace", 2025}, - new Object[] {"War and Peace", 1869})); + new Object[] {"War and Peace", 1869}), + Set.of(Book.COLLECTION_NAME)); } @Nested @DomainModel(annotatedClasses = Book.class) @ServiceRegistry(settings = @Setting(name = DEFAULT_NULL_ORDERING, value = "first")) - class DefaultNullPrecedenceTests extends AbstractSelectionQueryIntegrationTests { + class DefaultNullPrecedenceTests extends AbstractQueryIntegrationTests { @Test void testDefaultNullPrecedenceFeatureNotSupported() { assertSelectQueryFailure( @@ -332,7 +341,8 @@ void testOrderBySimpleTuple() { ] } """, - getBooksByIds(2, 1, 3, 4, 5)); + getBooksByIds(2, 1, 3, 4, 5), + Set.of(Book.COLLECTION_NAME)); } @Test @@ -365,7 +375,8 @@ void testOrderByNestedTuple() { ] } """, - getBooksByIds(5, 1, 4, 2, 3)); + getBooksByIds(5, 1, 4, 2, 3), + Set.of(Book.COLLECTION_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 4d60b1ef..93bc78b1 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -16,6 +16,7 @@ package com.mongodb.hibernate.internal.translate; +import static com.mongodb.hibernate.internal.MongoAssertions.assertFalse; import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertTrue; @@ -25,6 +26,7 @@ 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.MODEL_MUTATION_RESULT; 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; @@ -102,8 +104,11 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.AbstractMutationStatement; +import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.cte.CteContainer; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression; import org.hibernate.sql.ast.tree.expression.Any; @@ -266,10 +271,10 @@ public void visitStandardTableInsert(TableInsertStandard tableInsert) { astElements.add(new AstElement(fieldName, fieldValue)); } astVisitorValueHolder.yield( - MUTATION_RESULT, + MODEL_MUTATION_RESULT, ModelMutationMqlTranslator.Result.create( new AstInsertCommand( - tableInsert.getMutatingTable().getTableName(), new AstDocument(astElements)), + tableInsert.getMutatingTable().getTableName(), List.of(new AstDocument(astElements))), parameterBinders)); } @@ -288,7 +293,7 @@ public void visitStandardTableDelete(TableDeleteStandard tableDelete) { } var keyFilter = getKeyFilter(tableDelete); astVisitorValueHolder.yield( - MUTATION_RESULT, + MODEL_MUTATION_RESULT, ModelMutationMqlTranslator.Result.create( new AstDeleteCommand(tableDelete.getMutatingTable().getTableName(), keyFilter), parameterBinders)); @@ -310,7 +315,7 @@ public void visitStandardTableUpdate(TableUpdateStandard tableUpdate) { updates.add(new AstFieldUpdate(fieldName, fieldValue)); } astVisitorValueHolder.yield( - MUTATION_RESULT, + MODEL_MUTATION_RESULT, ModelMutationMqlTranslator.Result.create( new AstUpdateCommand(tableUpdate.getMutatingTable().getTableName(), keyFilter, updates), parameterBinders)); @@ -344,10 +349,7 @@ public void visitSelectStatement(SelectStatement selectStatement) { if (!selectStatement.getQueryPart().isRoot()) { throw new FeatureNotSupportedException("Subquery not supported"); } - if (!selectStatement.getCteStatements().isEmpty() - || !selectStatement.getCteObjects().isEmpty()) { - throw new FeatureNotSupportedException("CTE not supported"); - } + checkCteContainerSupportability(selectStatement); selectStatement.getQueryPart().accept(this); } @@ -475,16 +477,9 @@ private AstProjectStage createProjectStage(SelectClause selectClause) { @Override public void visitFromClause(FromClause fromClause) { - if (fromClause.getRoots().size() != 1) { - throw new FeatureNotSupportedException(); - } + checkFromClauseSupportability(fromClause); var tableGroup = fromClause.getRoots().get(0); - - if (!(tableGroup.getModelPart() instanceof EntityPersister entityPersister) - || entityPersister.getQuerySpaces().length != 1) { - throw new FeatureNotSupportedException(); - } - + var entityPersister = (EntityPersister) tableGroup.getModelPart(); affectedTableNames.add(((String[]) entityPersister.getQuerySpaces())[0]); tableGroup.getPrimaryTableReference().accept(this); } @@ -666,17 +661,97 @@ public void visitTuple(SqlTuple sqlTuple) { @Override public void visitDeleteStatement(DeleteStatement deleteStatement) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46"); + checkMutationStatementSupportability(deleteStatement); + var collectionAndAstFilter = getCollectionAndFilter(deleteStatement); + affectedTableNames.add(collectionAndAstFilter.collection); + astVisitorValueHolder.yield( + MUTATION_RESULT, + new MutationMqlTranslator.Result( + new AstDeleteCommand(collectionAndAstFilter.collection, collectionAndAstFilter.filter), + parameterBinders, + affectedTableNames)); } @Override public void visitUpdateStatement(UpdateStatement updateStatement) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46"); + checkMutationStatementSupportability(updateStatement); + var collectionAndAstFilter = getCollectionAndFilter(updateStatement); + affectedTableNames.add(collectionAndAstFilter.collection); + + var assignments = updateStatement.getAssignments(); + var fieldUpdates = new ArrayList(assignments.size()); + for (var assignment : assignments) { + var fieldReferences = assignment.getAssignable().getColumnReferences(); + assertTrue(fieldReferences.size() == 1); + + var fieldPath = acceptAndYield(fieldReferences.get(0), FIELD_PATH); + var assignedValue = assignment.getAssignedValue(); + if (!isValueExpression(assignedValue)) { + throw new FeatureNotSupportedException(); + } + var fieldValue = acceptAndYield(assignedValue, VALUE); + fieldUpdates.add(new AstFieldUpdate(fieldPath, fieldValue)); + } + astVisitorValueHolder.yield( + MUTATION_RESULT, + new MutationMqlTranslator.Result( + new AstUpdateCommand( + collectionAndAstFilter.collection, collectionAndAstFilter.filter, fieldUpdates), + parameterBinders, + affectedTableNames)); + } + + private CollectionAndFilter getCollectionAndFilter(AbstractUpdateOrDeleteStatement updateOrDeleteStatement) { + var collection = updateOrDeleteStatement.getTargetTable().getTableExpression(); + var astFilter = acceptAndYield(updateOrDeleteStatement.getRestriction(), FILTER); + return new CollectionAndFilter(collection, astFilter); } @Override - public void visitInsertStatement(InsertSelectStatement insertSelectStatement) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46"); + public void visitInsertStatement(InsertSelectStatement insertStatement) { + checkMutationStatementSupportability(insertStatement); + if (insertStatement.getConflictClause() != null) { + throw new FeatureNotSupportedException("Conflict clause in insert statement not supported"); + } + if (insertStatement.getSourceSelectStatement() != null) { + throw new FeatureNotSupportedException("Insertion statement with source selection not supported"); + } + + var collection = insertStatement.getTargetTable().getTableExpression(); + affectedTableNames.add(collection); + + var fieldReferences = insertStatement.getTargetColumns(); + assertFalse(fieldReferences.isEmpty()); + + var fieldNames = new ArrayList(fieldReferences.size()); + for (var fieldReference : fieldReferences) { + fieldNames.add(fieldReference.getColumnExpression()); + } + + var valuesList = insertStatement.getValuesList(); + assertFalse(valuesList.isEmpty()); + + var documents = new ArrayList(valuesList.size()); + for (var values : valuesList) { + var fieldValueExpressions = values.getExpressions(); + assertTrue(fieldNames.size() == fieldValueExpressions.size()); + var astElements = new ArrayList(fieldValueExpressions.size()); + for (var i = 0; i < fieldNames.size(); i++) { + var fieldName = fieldNames.get(i); + var fieldValueExpression = fieldValueExpressions.get(i); + if (!isValueExpression(fieldValueExpression)) { + throw new FeatureNotSupportedException(); + } + var fieldValue = acceptAndYield(fieldValueExpression, VALUE); + astElements.add(new AstElement(fieldName, fieldValue)); + } + documents.add(new AstDocument(astElements)); + } + + astVisitorValueHolder.yield( + MUTATION_RESULT, + new MutationMqlTranslator.Result( + new AstInsertCommand(collection, documents), parameterBinders, affectedTableNames)); } @Override @@ -1039,6 +1114,39 @@ private static BsonValue toBsonValue(Object value) { } } + private static void checkCteContainerSupportability(CteContainer cteContainer) { + if (!cteContainer.getCteStatements().isEmpty() + || !cteContainer.getCteObjects().isEmpty()) { + throw new FeatureNotSupportedException("CTE not supported"); + } + } + + private static void checkMutationStatementSupportability(AbstractMutationStatement mutationStatement) { + checkCteContainerSupportability(mutationStatement); + if (!mutationStatement.getReturningColumns().isEmpty()) { + throw new FeatureNotSupportedException("Returning columns from mutation statements not supported"); + } + if (mutationStatement instanceof AbstractUpdateOrDeleteStatement updateOrDeleteStatement) { + checkFromClauseSupportability(updateOrDeleteStatement.getFromClause()); + } + } + + private static void checkFromClauseSupportability(FromClause fromClause) { + if (fromClause.getRoots().size() != 1) { + throw new FeatureNotSupportedException("Only single root from clause is supported"); + } + var root = fromClause.getRoots().get(0); + if (root.hasRealJoins()) { + throw new FeatureNotSupportedException("TODO-HIBERNATE-65 https://jira.mongodb.org/browse/HIBERNATE-65"); + } + if (!(root.getModelPart() instanceof EntityPersister entityPersister) + || entityPersister.getQuerySpaces().length != 1) { + throw new FeatureNotSupportedException("Only single table from clause is supported"); + } + } + + private record CollectionAndFilter(String collection, AstFilter filter) {} + private static final class OffsetJdbcParameter extends AbstractJdbcParameter { OffsetJdbcParameter(BasicType 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 ad844f63..2214636e 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java @@ -33,10 +33,12 @@ @SuppressWarnings("UnusedTypeParameter") final class AstVisitorValueDescriptor { - static final AstVisitorValueDescriptor MUTATION_RESULT = + static final AstVisitorValueDescriptor MODEL_MUTATION_RESULT = new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor SELECT_RESULT = new AstVisitorValueDescriptor<>(); + static final AstVisitorValueDescriptor MUTATION_RESULT = + new AstVisitorValueDescriptor<>(); static final AstVisitorValueDescriptor COLLECTION_NAME = new AstVisitorValueDescriptor<>(); 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 90b75c40..71b56ec6 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/ModelMutationMqlTranslator.java @@ -18,7 +18,7 @@ import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; import static com.mongodb.hibernate.internal.MongoAssertions.assertNull; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MUTATION_RESULT; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MODEL_MUTATION_RESULT; import static java.util.Collections.emptyList; import com.mongodb.hibernate.internal.translate.mongoast.command.AstCommand; @@ -50,7 +50,7 @@ public O translate(@Nullable JdbcParameterBindings jdbcParameterBindings, QueryO if ((TableMutation) tableMutation instanceof TableUpdateNoSet) { result = Result.empty(); } else { - result = acceptAndYield(tableMutation, MUTATION_RESULT); + result = acceptAndYield(tableMutation, MODEL_MUTATION_RESULT); } return result.createJdbcMutationOperation(tableMutation); } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java b/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java index 9a1a87fe..e596b89e 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/MongoTranslatorFactory.java @@ -36,8 +36,7 @@ public SqlAstTranslator buildSelectTranslator( @Override public SqlAstTranslator buildMutationTranslator( SessionFactoryImplementor sessionFactoryImplementor, MutationStatement mutationStatement) { - // TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46 - return new NoopSqlAstTranslator<>(); + return new MutationMqlTranslator(sessionFactoryImplementor, mutationStatement); } @Override diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/MutationMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/MutationMqlTranslator.java new file mode 100644 index 00000000..58de052e --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/MutationMqlTranslator.java @@ -0,0 +1,90 @@ +/* + * 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; + +import static com.mongodb.hibernate.internal.MongoAssertions.fail; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MUTATION_RESULT; +import static java.lang.String.format; +import static java.util.Collections.emptyMap; +import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; + +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; +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.MutationStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQueryInsertImpl; +import org.hibernate.sql.exec.spi.JdbcOperationQueryDelete; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcOperationQueryUpdate; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.jspecify.annotations.Nullable; + +final class MutationMqlTranslator extends AbstractMqlTranslator { + + private final MutationStatement mutationStatement; + + MutationMqlTranslator(SessionFactoryImplementor sessionFactory, MutationStatement mutationStatement) { + super(sessionFactory); + this.mutationStatement = mutationStatement; + } + + @Override + public JdbcOperationQueryMutation translate( + @Nullable JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { + + logSqlAst(mutationStatement); + + checkJdbcParameterBindingsSupportability(jdbcParameterBindings); + applyQueryOptions(queryOptions); + + var result = acceptAndYield(mutationStatement, MUTATION_RESULT); + return result.createJdbcOperationQueryMutation(); + } + + static final class Result { + private final AstCommand command; + private final List parameterBinders; + private final Set affectedTableNames; + + Result(AstCommand command, List parameterBinders, Set affectedTableNames) { + this.command = command; + this.parameterBinders = parameterBinders; + this.affectedTableNames = affectedTableNames; + } + + private JdbcOperationQueryMutation createJdbcOperationQueryMutation() { + var mql = renderMongoAstNode(command); + if (command instanceof AstInsertCommand) { + return new JdbcOperationQueryInsertImpl(mql, parameterBinders, affectedTableNames); + } else if (command instanceof AstUpdateCommand) { + return new JdbcOperationQueryUpdate(mql, parameterBinders, affectedTableNames, emptyMap()); + } else if (command instanceof AstDeleteCommand) { + return new JdbcOperationQueryDelete(mql, parameterBinders, affectedTableNames, emptyMap()); + } else { + throw fail(format( + "Unexpected mutation command type: %s", + command.getClass().getName())); + } + } + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/NoopSqlAstTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/NoopSqlAstTranslator.java deleted file mode 100644 index 5eb6c2d1..00000000 --- a/src/main/java/com/mongodb/hibernate/internal/translate/NoopSqlAstTranslator.java +++ /dev/null @@ -1,371 +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; - -import java.util.Set; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.internal.util.collections.Stack; -import org.hibernate.persister.internal.SqlFragmentPredicate; -import org.hibernate.query.spi.QueryOptions; -import org.hibernate.query.sqm.tree.expression.Conversion; -import org.hibernate.sql.ast.Clause; -import org.hibernate.sql.ast.SqlAstNodeRenderingMode; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.spi.SqlSelection; -import org.hibernate.sql.ast.tree.SqlAstNode; -import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression; -import org.hibernate.sql.ast.tree.expression.Any; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; -import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; -import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; -import org.hibernate.sql.ast.tree.expression.CastTarget; -import org.hibernate.sql.ast.tree.expression.Collation; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.Distinct; -import org.hibernate.sql.ast.tree.expression.Duration; -import org.hibernate.sql.ast.tree.expression.DurationUnit; -import org.hibernate.sql.ast.tree.expression.EmbeddableTypeLiteral; -import org.hibernate.sql.ast.tree.expression.EntityTypeLiteral; -import org.hibernate.sql.ast.tree.expression.Every; -import org.hibernate.sql.ast.tree.expression.ExtractUnit; -import org.hibernate.sql.ast.tree.expression.Format; -import org.hibernate.sql.ast.tree.expression.JdbcLiteral; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.expression.ModifiedSubQueryExpression; -import org.hibernate.sql.ast.tree.expression.NestedColumnReference; -import org.hibernate.sql.ast.tree.expression.Over; -import org.hibernate.sql.ast.tree.expression.Overflow; -import org.hibernate.sql.ast.tree.expression.QueryLiteral; -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.Star; -import org.hibernate.sql.ast.tree.expression.Summarization; -import org.hibernate.sql.ast.tree.expression.TrimSpecification; -import org.hibernate.sql.ast.tree.expression.UnaryOperation; -import org.hibernate.sql.ast.tree.expression.UnparsedNumericLiteral; -import org.hibernate.sql.ast.tree.from.FromClause; -import org.hibernate.sql.ast.tree.from.FunctionTableReference; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.QueryPartTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableGroupJoin; -import org.hibernate.sql.ast.tree.from.TableReferenceJoin; -import org.hibernate.sql.ast.tree.from.ValuesTableReference; -import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; -import org.hibernate.sql.ast.tree.predicate.BetweenPredicate; -import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; -import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; -import org.hibernate.sql.ast.tree.predicate.ExistsPredicate; -import org.hibernate.sql.ast.tree.predicate.FilterPredicate; -import org.hibernate.sql.ast.tree.predicate.GroupedPredicate; -import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; -import org.hibernate.sql.ast.tree.predicate.InListPredicate; -import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; -import org.hibernate.sql.ast.tree.predicate.Junction; -import org.hibernate.sql.ast.tree.predicate.LikePredicate; -import org.hibernate.sql.ast.tree.predicate.NegatedPredicate; -import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; -import org.hibernate.sql.ast.tree.predicate.SelfRenderingPredicate; -import org.hibernate.sql.ast.tree.predicate.ThruthnessPredicate; -import org.hibernate.sql.ast.tree.select.QueryGroup; -import org.hibernate.sql.ast.tree.select.QueryPart; -import org.hibernate.sql.ast.tree.select.QuerySpec; -import org.hibernate.sql.ast.tree.select.SelectClause; -import org.hibernate.sql.ast.tree.select.SelectStatement; -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.spi.JdbcOperation; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.model.ast.ColumnWriteFragment; -import org.hibernate.sql.model.internal.OptionalTableUpdate; -import org.hibernate.sql.model.internal.TableDeleteCustomSql; -import org.hibernate.sql.model.internal.TableDeleteStandard; -import org.hibernate.sql.model.internal.TableInsertCustomSql; -import org.hibernate.sql.model.internal.TableInsertStandard; -import org.hibernate.sql.model.internal.TableUpdateCustomSql; -import org.hibernate.sql.model.internal.TableUpdateStandard; -import org.jspecify.annotations.NullUnmarked; - -@NullUnmarked -final class NoopSqlAstTranslator implements SqlAstTranslator { - - NoopSqlAstTranslator() {} - - @Override - public SessionFactoryImplementor getSessionFactory() { - return null; - } - - @Override - public void render(SqlAstNode sqlAstNode, SqlAstNodeRenderingMode renderingMode) {} - - @Override - public boolean supportsFilterClause() { - return false; - } - - @Override - public QueryPart getCurrentQueryPart() { - return null; - } - - @Override - public Stack getCurrentClauseStack() { - return null; - } - - @Override - public Set getAffectedTableNames() { - return Set.of(); - } - - @Override - public T translate(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { - return null; - } - - @Override - public void visitSelectStatement(SelectStatement statement) {} - - @Override - public void visitDeleteStatement(DeleteStatement statement) {} - - @Override - public void visitUpdateStatement(UpdateStatement statement) {} - - @Override - public void visitInsertStatement(InsertSelectStatement statement) {} - - @Override - public void visitAssignment(Assignment assignment) {} - - @Override - public void visitQueryGroup(QueryGroup queryGroup) {} - - @Override - public void visitQuerySpec(QuerySpec querySpec) {} - - @Override - public void visitSortSpecification(SortSpecification sortSpecification) {} - - @Override - public void visitOffsetFetchClause(QueryPart querySpec) {} - - @Override - public void visitSelectClause(SelectClause selectClause) {} - - @Override - public void visitSqlSelection(SqlSelection sqlSelection) {} - - @Override - public void visitFromClause(FromClause fromClause) {} - - @Override - public void visitTableGroup(TableGroup tableGroup) {} - - @Override - public void visitTableGroupJoin(TableGroupJoin tableGroupJoin) {} - - @Override - public void visitNamedTableReference(NamedTableReference tableReference) {} - - @Override - public void visitValuesTableReference(ValuesTableReference tableReference) {} - - @Override - public void visitQueryPartTableReference(QueryPartTableReference tableReference) {} - - @Override - public void visitFunctionTableReference(FunctionTableReference tableReference) {} - - @Override - public void visitTableReferenceJoin(TableReferenceJoin tableReferenceJoin) {} - - @Override - public void visitColumnReference(ColumnReference columnReference) {} - - @Override - public void visitNestedColumnReference(NestedColumnReference nestedColumnReference) {} - - @Override - public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) {} - - @Override - public void visitExtractUnit(ExtractUnit extractUnit) {} - - @Override - public void visitFormat(Format format) {} - - @Override - public void visitDistinct(Distinct distinct) {} - - @Override - public void visitOverflow(Overflow overflow) {} - - @Override - public void visitStar(Star star) {} - - @Override - public void visitTrimSpecification(TrimSpecification trimSpecification) {} - - @Override - public void visitCastTarget(CastTarget castTarget) {} - - @Override - public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) {} - - @Override - public void visitCaseSearchedExpression(CaseSearchedExpression caseSearchedExpression) {} - - @Override - public void visitCaseSimpleExpression(CaseSimpleExpression caseSimpleExpression) {} - - @Override - public void visitAny(Any any) {} - - @Override - public void visitEvery(Every every) {} - - @Override - public void visitSummarization(Summarization every) {} - - @Override - public void visitOver(Over over) {} - - @Override - public void visitSelfRenderingExpression(SelfRenderingExpression expression) {} - - @Override - public void visitSqlSelectionExpression(SqlSelectionExpression expression) {} - - @Override - public void visitEntityTypeLiteral(EntityTypeLiteral expression) {} - - @Override - public void visitEmbeddableTypeLiteral(EmbeddableTypeLiteral expression) {} - - @Override - public void visitTuple(SqlTuple tuple) {} - - @Override - public void visitCollation(Collation collation) {} - - @Override - public void visitParameter(JdbcParameter jdbcParameter) {} - - @Override - public void visitJdbcLiteral(JdbcLiteral jdbcLiteral) {} - - @Override - public void visitQueryLiteral(QueryLiteral queryLiteral) {} - - @Override - public void visitUnparsedNumericLiteral(UnparsedNumericLiteral literal) {} - - @Override - public void visitUnaryOperationExpression(UnaryOperation unaryOperationExpression) {} - - @Override - public void visitModifiedSubQueryExpression(ModifiedSubQueryExpression expression) {} - - @Override - public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) {} - - @Override - public void visitBetweenPredicate(BetweenPredicate betweenPredicate) {} - - @Override - public void visitFilterPredicate(FilterPredicate filterPredicate) {} - - @Override - public void visitFilterFragmentPredicate(FilterPredicate.FilterFragmentPredicate fragmentPredicate) {} - - @Override - public void visitSqlFragmentPredicate(SqlFragmentPredicate predicate) {} - - @Override - public void visitGroupedPredicate(GroupedPredicate groupedPredicate) {} - - @Override - public void visitInListPredicate(InListPredicate inListPredicate) {} - - @Override - public void visitInSubQueryPredicate(InSubQueryPredicate inSubQueryPredicate) {} - - @Override - public void visitInArrayPredicate(InArrayPredicate inArrayPredicate) {} - - @Override - public void visitExistsPredicate(ExistsPredicate existsPredicate) {} - - @Override - public void visitJunction(Junction junction) {} - - @Override - public void visitLikePredicate(LikePredicate likePredicate) {} - - @Override - public void visitNegatedPredicate(NegatedPredicate negatedPredicate) {} - - @Override - public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) {} - - @Override - public void visitThruthnessPredicate(ThruthnessPredicate predicate) {} - - @Override - public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) {} - - @Override - public void visitSelfRenderingPredicate(SelfRenderingPredicate selfRenderingPredicate) {} - - @Override - public void visitDurationUnit(DurationUnit durationUnit) {} - - @Override - public void visitDuration(Duration duration) {} - - @Override - public void visitConversion(Conversion conversion) {} - - @Override - public void visitStandardTableInsert(TableInsertStandard tableInsert) {} - - @Override - public void visitCustomTableInsert(TableInsertCustomSql tableInsert) {} - - @Override - public void visitStandardTableDelete(TableDeleteStandard tableDelete) {} - - @Override - public void visitCustomTableDelete(TableDeleteCustomSql tableDelete) {} - - @Override - public void visitStandardTableUpdate(TableUpdateStandard tableUpdate) {} - - @Override - public void visitOptionalTableUpdate(OptionalTableUpdate tableUpdate) {} - - @Override - public void visitCustomTableUpdate(TableUpdateCustomSql tableUpdate) {} - - @Override - public void visitColumnWriteFragment(ColumnWriteFragment columnWriteFragment) {} -} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java index a8358991..890bb008 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommand.java @@ -16,10 +16,18 @@ package com.mongodb.hibernate.internal.translate.mongoast.command; +import static com.mongodb.hibernate.internal.MongoAssertions.assertFalse; + import com.mongodb.hibernate.internal.translate.mongoast.AstDocument; +import java.util.List; import org.bson.BsonWriter; -public record AstInsertCommand(String collection, AstDocument document) implements AstCommand { +public record AstInsertCommand(String collection, List documents) implements AstCommand { + + public AstInsertCommand { + assertFalse(documents.isEmpty()); + } + @Override public void render(BsonWriter writer) { writer.writeStartDocument(); @@ -28,7 +36,7 @@ public void render(BsonWriter writer) { writer.writeName("documents"); writer.writeStartArray(); { - document.render(writer); + documents.forEach(document -> document.render(writer)); } writer.writeEndArray(); } 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 f75244fe..c6427554 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("MUTATION_RESULT", AstVisitorValueDescriptor.MUTATION_RESULT.toString()); + assertEquals("MODEL_MUTATION_RESULT", AstVisitorValueDescriptor.MODEL_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 c9865e63..c8475f10 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java @@ -16,7 +16,7 @@ package com.mongodb.hibernate.internal.translate; -import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MUTATION_RESULT; +import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.MODEL_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; @@ -63,12 +63,12 @@ void testRecursiveUsage() { var fieldValue = astVisitorValueHolder.execute(VALUE, fieldValueYielder); AstElement astElement = new AstElement("province", fieldValue); astVisitorValueHolder.yield( - MUTATION_RESULT, + MODEL_MUTATION_RESULT, ModelMutationMqlTranslator.Result.create( - new AstInsertCommand("city", new AstDocument(List.of(astElement))), emptyList())); + new AstInsertCommand("city", List.of(new AstDocument(List.of(astElement)))), emptyList())); }; - astVisitorValueHolder.execute(MUTATION_RESULT, tableInserter); + astVisitorValueHolder.execute(MODEL_MUTATION_RESULT, tableInserter); } @Test @@ -90,7 +90,7 @@ void testHolderExpectingDifferentDescriptor() { Runnable valueYielder = () -> astVisitorValueHolder.yield(VALUE, new AstLiteralValue(new BsonString("some_value"))); - assertThrows(Error.class, () -> astVisitorValueHolder.execute(MUTATION_RESULT, valueYielder)); + assertThrows(Error.class, () -> astVisitorValueHolder.execute(MODEL_MUTATION_RESULT, valueYielder)); } @Test diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java index c26f212a..78d99307 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/AstInsertCommandTests.java @@ -33,15 +33,24 @@ class AstInsertCommandTests { void testRendering() { var collection = "books"; - var elements = List.of( + + var elements1 = List.of( new AstElement("title", new AstLiteralValue(new BsonString("War and Peace"))), new AstElement("year", new AstLiteralValue(new BsonInt32(1867))), new AstElement("_id", AstParameterMarker.INSTANCE)); - var insertCommand = new AstInsertCommand(collection, new AstDocument(elements)); + var document1 = new AstDocument(elements1); + + var elements2 = List.of( + new AstElement("title", new AstLiteralValue(new BsonString("Crime and Punishment"))), + new AstElement("year", new AstLiteralValue(new BsonInt32(1868))), + new AstElement("_id", AstParameterMarker.INSTANCE)); + var document2 = new AstDocument(elements2); + + var insertCommand = new AstInsertCommand(collection, List.of(document1, document2)); var expectedJson = """ - {"insert": "books", "documents": [{"title": "War and Peace", "year": {"$numberInt": "1867"}, "_id": {"$undefined": true}}]}\ + {"insert": "books", "documents": [{"title": "War and Peace", "year": {"$numberInt": "1867"}, "_id": {"$undefined": true}}, {"title": "Crime and Punishment", "year": {"$numberInt": "1868"}, "_id": {"$undefined": true}}]}\ """; assertRendering(expectedJson, insertCommand); }