diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java similarity index 72% rename from src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java rename to src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java index 0ddd2aac..b1b876c4 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/AbstractQueryIntegrationTests.java @@ -14,18 +14,19 @@ * limitations under the License. */ -package com.mongodb.hibernate.query.select; +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 com.mongodb.client.MongoCollection; 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.MutationQuery; import org.hibernate.query.SelectionQuery; import org.hibernate.testing.orm.junit.ServiceRegistryScope; import org.hibernate.testing.orm.junit.ServiceRegistryScopeAware; @@ -36,17 +37,17 @@ @SessionFactory(exportSchema = false) @ExtendWith(MongoExtension.class) -abstract class AbstractSelectionQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { +public abstract class AbstractQueryIntegrationTests implements SessionFactoryScopeAware, ServiceRegistryScopeAware { private SessionFactoryScope sessionFactoryScope; private TestCommandListener testCommandListener; - SessionFactoryScope getSessionFactoryScope() { + protected SessionFactoryScope getSessionFactoryScope() { return sessionFactoryScope; } - TestCommandListener getTestCommandListener() { + protected TestCommandListener getTestCommandListener() { return testCommandListener; } @@ -60,12 +61,12 @@ public void injectServiceRegistryScope(ServiceRegistryScope serviceRegistryScope this.testCommandListener = serviceRegistryScope.getRegistry().requireService(TestCommandListener.class); } - void assertSelectionQuery( + protected void assertSelectionQuery( String hql, Class resultType, Consumer> queryPostProcessor, String expectedMql, - List expectedResultList) { + Iterable expectedResultList) { assertSelectionQuery( hql, resultType, @@ -74,16 +75,17 @@ void assertSelectionQuery( resultList -> assertIterableEq(expectedResultList, resultList)); } - void assertSelectionQuery(String hql, Class resultType, String expectedMql, List expectedResultList) { + protected void assertSelectionQuery( + String hql, Class resultType, String expectedMql, Iterable expectedResultList) { assertSelectionQuery(hql, resultType, null, expectedMql, expectedResultList); } - void assertSelectionQuery( + protected void assertSelectionQuery( String hql, Class resultType, Consumer> queryPostProcessor, String expectedMql, - Consumer> resultListVerifier) { + Consumer> resultListVerifier) { sessionFactoryScope.inTransaction(session -> { var selectionQuery = session.createSelectionQuery(hql, resultType); if (queryPostProcessor != null) { @@ -97,12 +99,12 @@ void assertSelectionQuery( }); } - void assertSelectionQuery( - String hql, Class resultType, String expectedMql, Consumer> resultListVerifier) { + protected void assertSelectionQuery( + String hql, Class resultType, String expectedMql, Consumer> resultListVerifier) { assertSelectionQuery(hql, resultType, null, expectedMql, resultListVerifier); } - void assertSelectQueryFailure( + protected void assertSelectQueryFailure( String hql, Class resultType, Consumer> queryPostProcessor, @@ -120,7 +122,7 @@ void assertSelectQueryFailure( .hasMessage(expectedExceptionMessage, expectedExceptionMessageParameters)); } - void assertSelectQueryFailure( + protected void assertSelectQueryFailure( String hql, Class resultType, Class expectedExceptionType, @@ -135,7 +137,7 @@ void assertSelectQueryFailure( expectedExceptionMessageParameters); } - void assertActualCommand(BsonDocument expectedCommand) { + protected void assertActualCommand(BsonDocument expectedCommand) { var capturedCommands = testCommandListener.getStartedCommands(); assertThat(capturedCommands) @@ -143,4 +145,23 @@ void assertActualCommand(BsonDocument expectedCommand) { .asInstanceOf(InstanceOfAssertFactories.MAP) .containsAllEntriesOf(expectedCommand); } + + protected void assertMutationQuery( + String hql, + Consumer queryPostProcessor, + int expectedMutationCount, + String expectedMql, + MongoCollection collection, + Iterable expectedDocuments) { + 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); + } } 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 67% 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..8a1e0fa8 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) +public class Book { + public static final String COLLECTION = "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/AbstractMutationQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/AbstractMutationQueryIntegrationTests.java new file mode 100644 index 00000000..1d985a32 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/AbstractMutationQueryIntegrationTests.java @@ -0,0 +1,101 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +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.hibernate.dialect.MongoDialect; +import com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +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.AbstractJdbcOperationQuery; +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.Setting; +import org.mockito.stubbing.Answer; + +@ServiceRegistry( + settings = + @Setting( + name = DIALECT, + value = + "com.mongodb.hibernate.query.mutation.AbstractMutationQueryIntegrationTests$MutationTranslateResultAwareDialect")) +public class AbstractMutationQueryIntegrationTests extends AbstractQueryIntegrationTests { + + protected void assertExpectedAffectedCollections(String... expectedAffectedfCollections) { + assertThat(((MutationTranslateResultAwareDialect) getSessionFactoryScope() + .getSessionFactory() + .getJdbcServices() + .getDialect()) + .capturedTranslateResult.getAffectedTableNames()) + .containsExactlyInAnyOrder(expectedAffectedfCollections); + } + + public static final class MutationTranslateResultAwareDialect extends Dialect { + private final Dialect delegate; + private AbstractJdbcOperationQuery capturedTranslateResult; + + public MutationTranslateResultAwareDialect(DialectResolutionInfo info) { + super(info); + delegate = new MongoDialect(info); + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new SqlAstTranslatorFactory() { + @Override + public SqlAstTranslator buildSelectTranslator( + SessionFactoryImplementor sessionFactory, SelectStatement statement) { + return delegate.getSqlAstTranslatorFactory().buildSelectTranslator(sessionFactory, statement); + } + + @Override + public SqlAstTranslator buildMutationTranslator( + SessionFactoryImplementor sessionFactory, MutationStatement statement) { + var originalTranslator = + delegate.getSqlAstTranslatorFactory().buildMutationTranslator(sessionFactory, statement); + var translatorSpy = spy(originalTranslator); + doAnswer((Answer) invocation -> { + capturedTranslateResult = (AbstractJdbcOperationQuery) invocation.callRealMethod(); + return capturedTranslateResult; + }) + .when(translatorSpy) + .translate(any(), any()); + return translatorSpy; + } + + @Override + public SqlAstTranslator buildModelMutationTranslator( + TableMutation mutation, SessionFactoryImplementor sessionFactory) { + return delegate.getSqlAstTranslatorFactory().buildModelMutationTranslator(mutation, sessionFactory); + } + }; + } + } +} 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..fba94527 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java @@ -0,0 +1,194 @@ +/* + * 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.Book; +import java.util.List; +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 AbstractMutationQueryIntegrationTests { + + @InjectMongoCollection(Book.COLLECTION) + 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"} + } + """))); + assertExpectedAffectedCollections(Book.COLLECTION); + } + + @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"} + } + """))); + assertExpectedAffectedCollections(Book.COLLECTION); + } +} 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..2a9e76a2 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/InsertionIntegrationTests.java @@ -0,0 +1,142 @@ +/* + * 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.Book; +import java.util.List; +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 InsertionIntegrationTests extends AbstractMutationQueryIntegrationTests { + + @InjectMongoCollection(Book.COLLECTION) + 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"} + } + """))); + assertExpectedAffectedCollections(Book.COLLECTION); + } + + @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"} + } + """))); + assertExpectedAffectedCollections(Book.COLLECTION); + } +} 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..de23046f --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java @@ -0,0 +1,229 @@ +/* + * 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.Book; +import java.util.List; +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 AbstractMutationQueryIntegrationTests { + + @InjectMongoCollection(Book.COLLECTION) + 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 + 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"} + } + """))); + assertExpectedAffectedCollections(Book.COLLECTION); + } + + @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"} + } + """))); + assertExpectedAffectedCollections(Book.COLLECTION); + } +} 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..d7682b90 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java @@ -19,13 +19,15 @@ 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 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; @@ -54,15 +56,38 @@ 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}}]}", + """ + { + "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)); } @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/SimpleSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java index 5e454851..f81cf810 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -20,6 +20,8 @@ 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; @@ -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 { 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..bd48ad4c 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -25,6 +25,8 @@ 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 org.hibernate.testing.orm.junit.DomainModel; @@ -37,7 +39,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), @@ -252,7 +254,7 @@ void testSortFieldByOrdinalReference() { @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( 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 22df389a..30f4c73c 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.assertTrue; import static com.mongodb.hibernate.internal.MongoConstants.EXTENDED_JSON_WRITER_SETTINGS; @@ -95,8 +96,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; @@ -259,7 +263,8 @@ public void visitStandardTableInsert(TableInsertStandard tableInsert) { } astVisitorValueHolder.yield( COLLECTION_MUTATION, - new AstInsertCommand(tableInsert.getMutatingTable().getTableName(), new AstDocument(astElements))); + new AstInsertCommand( + tableInsert.getMutatingTable().getTableName(), List.of(new AstDocument(astElements)))); } @Override @@ -329,10 +334,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); } @@ -385,16 +387,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); } @@ -576,17 +571,87 @@ 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( + COLLECTION_MUTATION, + new AstDeleteCommand(collectionAndAstFilter.collection, collectionAndAstFilter.filter)); } @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, FIELD_VALUE); + fieldUpdates.add(new AstFieldUpdate(fieldPath, fieldValue)); + } + astVisitorValueHolder.yield( + COLLECTION_MUTATION, + new AstUpdateCommand(collectionAndAstFilter.collection, collectionAndAstFilter.filter, fieldUpdates)); + } + + 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(); + } + if (insertStatement.getSourceSelectStatement() != null) { + throw new FeatureNotSupportedException(); + } + + 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, FIELD_VALUE); + astElements.add(new AstElement(fieldName, fieldValue)); + } + documents.add(new AstDocument(astElements)); + } + + astVisitorValueHolder.yield(COLLECTION_MUTATION, new AstInsertCommand(collection, documents)); } @Override @@ -956,4 +1021,34 @@ private static BsonValue toBsonValue(Object value) { throw new FeatureNotSupportedException(e); } } + + 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(); + } + 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.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) {} } 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..a6ad5253 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/MutationMqlTranslator.java @@ -0,0 +1,74 @@ +/* + * 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.COLLECTION_MUTATION; +import static java.lang.String.format; +import static java.util.Collections.emptyMap; +import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.insert.InsertStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +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.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); + checkQueryOptionsSupportability(queryOptions); + + var mutationCommand = acceptAndYield(mutationStatement, COLLECTION_MUTATION); + var mql = renderMongoAstNode(mutationCommand); + var parameterBinders = getParameterBinders(); + var affectedCollections = getAffectedTableNames(); + + // switch to Switch Pattern Matching when JDK is upgraded to 21+ + if (mutationStatement instanceof InsertStatement) { + return new JdbcOperationQueryInsertImpl(mql, parameterBinders, affectedCollections); + } else if (mutationStatement instanceof UpdateStatement) { + return new JdbcOperationQueryUpdate(mql, parameterBinders, affectedCollections, emptyMap()); + } else if (mutationStatement instanceof DeleteStatement) { + return new JdbcOperationQueryDelete(mql, parameterBinders, affectedCollections, emptyMap()); + } else { + throw fail(format( + "Unexpected mutation statement type: %s", + mutationStatement.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/AstVisitorValueHolderTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java index b11d20aa..d8dfa49d 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/AstVisitorValueHolderTests.java @@ -56,13 +56,11 @@ void testSimpleUsage() { void testRecursiveUsage() { Runnable tableInserter = () -> { - Runnable fieldValueYielder = () -> { - astVisitorValueHolder.yield(FIELD_VALUE, AstParameterMarker.INSTANCE); - }; + Runnable fieldValueYielder = () -> astVisitorValueHolder.yield(FIELD_VALUE, AstParameterMarker.INSTANCE); var fieldValue = astVisitorValueHolder.execute(FIELD_VALUE, fieldValueYielder); AstElement astElement = new AstElement("province", fieldValue); astVisitorValueHolder.yield( - COLLECTION_MUTATION, new AstInsertCommand("city", new AstDocument(List.of(astElement)))); + COLLECTION_MUTATION, new AstInsertCommand("city", List.of(new AstDocument(List.of(astElement))))); }; astVisitorValueHolder.execute(COLLECTION_MUTATION, tableInserter); 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..7975491e 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslatorTests.java @@ -88,7 +88,7 @@ void testAffectedTableNames( var translator = new SelectMqlTranslator(sessionFactory, selectFromTableName); - translator.translate(null, QueryOptions.NONE); + translator.translate(null, QueryOptions.NONE).getAffectedTableNames(); assertThat(translator.getAffectedTableNames()).containsExactly(tableName); } 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); }