diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java new file mode 100644 index 00000000..573304ab --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -0,0 +1,361 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.query.select; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mongodb.hibernate.MongoTestAssertions; +import com.mongodb.hibernate.annotations.ObjectIdGenerator; +import com.mongodb.hibernate.cfg.MongoConfigurator; +import com.mongodb.hibernate.junit.MongoExtension; +import com.mongodb.hibernate.service.spi.MongoConfigurationContributor; +import com.mongodb.hibernate.util.TestCommandListener; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.io.Serial; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import org.bson.BsonDocument; +import org.bson.types.ObjectId; +import org.hibernate.query.SelectionQuery; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@SessionFactory(exportSchema = false) +@DomainModel( + annotatedClasses = { + SimpleSelectQueryIntegrationTests.Contact.class, + SimpleSelectQueryIntegrationTests.Book.class + }) +@ServiceRegistry( + services = + @ServiceRegistry.Service( + role = MongoConfigurationContributor.class, + impl = SimpleSelectQueryIntegrationTests.TestingMongoConfigurationContributor.class)) +@ExtendWith(MongoExtension.class) +class SimpleSelectQueryIntegrationTests implements SessionFactoryScopeAware { + + public static class TestingMongoConfigurationContributor implements MongoConfigurationContributor { + + @Serial + private static final long serialVersionUID = 1L; + + @Override + public void configure(MongoConfigurator configurator) { + configurator.applyToMongoClientSettings(builder -> builder.addCommandListener(MONGO_COMMAND_LISTENER)); + } + } + + private static final TestCommandListener MONGO_COMMAND_LISTENER = new TestCommandListener(); + + private SessionFactoryScope sessionFactoryScope; + + @Override + public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { + this.sessionFactoryScope = sessionFactoryScope; + } + + @Nested + class QueryTests { + + private final List testingContacts = List.of( + new Contact(1, "Bob", 18, Country.USA), + new Contact(2, "Mary", 35, Country.CANADA), + new Contact(3, "Dylan", 7, Country.CANADA), + new Contact(4, "Lucy", 78, Country.CANADA), + new Contact(5, "John", 25, Country.USA)); + + private List getTestingContacts(int... ids) { + return Arrays.stream(ids) + .mapToObj(id -> testingContacts.get(id - 1)) + .toList(); + } + + @BeforeEach + void beforeEach() { + sessionFactoryScope.inTransaction(session -> testingContacts.forEach(session::persist)); + MONGO_COMMAND_LISTENER.clear(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByEq(boolean fieldAsLhs) { + assertContactQuery( + "from Contact where " + (fieldAsLhs ? "country = :country" : ":country = country"), + q -> q.setParameter("country", Country.USA.name()), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'country': {'$eq': 'USA'}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(1, 5)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByNe(boolean fieldAsLhs) { + assertContactQuery( + "from Contact where " + (fieldAsLhs ? "country != ?1" : "?1 != country"), + q -> q.setParameter(1, Country.USA.name()), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'country': {'$ne': 'USA'}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(2, 3, 4)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByLt(boolean fieldAsLhs) { + assertContactQuery( + "from Contact where " + (fieldAsLhs ? "age < :age" : ":age > age"), + q -> q.setParameter("age", 35), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'age': {'$lt': 35}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(1, 3, 5)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByLte(boolean fieldAsLhs) { + assertContactQuery( + "from Contact where " + (fieldAsLhs ? "age <= ?1" : "?1 >= age"), + q -> q.setParameter(1, 35), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'age': {'$lte': 35}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(1, 2, 3, 5)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByGt(boolean fieldAsLhs) { + assertContactQuery( + "from Contact where " + (fieldAsLhs ? "age > :age" : ":age < age"), + q -> q.setParameter("age", 18), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'age': {'$gt': 18}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(2, 4, 5)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByGte(boolean fieldAsLhs) { + assertContactQuery( + "from Contact where " + (fieldAsLhs ? "age >= :age" : ":age <= age"), + q -> q.setParameter("age", 18), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'age': {'$gte': 18}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(1, 2, 4, 5)); + } + + @Test + void testAndFilter() { + assertContactQuery( + "from Contact where country = ?1 and age > ?2", + q -> q.setParameter(1, Country.CANADA.name()).setParameter(2, 18), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'$and': [{'country': {'$eq': 'CANADA'}}, {'age': {'$gt': 18}}]}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(2, 4)); + } + + @Test + void testOrFilter() { + assertContactQuery( + "from Contact where country = :country or age > :age", + q -> q.setParameter("country", Country.CANADA.name()).setParameter("age", 18), + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'$or': [{'country': {'$eq': 'CANADA'}}, {'age': {'$gt': 18}}]}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(2, 3, 4, 5)); + } + + @Test + void testSingleNegation() { + assertContactQuery( + "from Contact where age > 18 and not (country = 'USA')", + null, + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'$and': [{'age': {'$gt': 18}}, {'$nor': [{'country': {'$eq': 'USA'}}]}]}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(2, 4)); + } + + @Test + void testDoubleNegation() { + assertContactQuery( + "from Contact where age > 18 and not ( not (country = 'USA') )", + null, + "{'aggregate': 'contacts', 'pipeline': [{'$match': {'$and': [{'age': {'$gt': 18}}, {'$nor': [{'$nor': [{'country': {'$eq': 'USA'}}]}]}]}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", + getTestingContacts(5)); + } + + private void assertContactQuery( + String hql, + Consumer> queryPostProcessor, + String expectedMql, + List expectedContacts) { + sessionFactoryScope.inTransaction(session -> { + var selectionQuery = session.createSelectionQuery(hql, Contact.class); + if (queryPostProcessor != null) { + queryPostProcessor.accept(selectionQuery); + } + var resultList = selectionQuery.getResultList(); + + var capturedCommands = MONGO_COMMAND_LISTENER.getFinishedCommands(); + + assertThat(capturedCommands) + .singleElement() + .extracting(TestCommandListener::getActualAggregateCommand) + .usingRecursiveComparison() + .isEqualTo(BsonDocument.parse(expectedMql)); + + assertThat(resultList) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyElementsOf(expectedContacts); + }); + } + + @Test + void testProjectOperation() { + sessionFactoryScope.inTransaction(session -> { + var results = session.createSelectionQuery( + "select name, age from Contact where country = :country", Object[].class) + .setParameter("country", Country.CANADA.name()) + .getResultList(); + assertThat(results) + .containsExactly( + new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78}); + }); + } + } + + @Nested + class QueryLiteralTests { + + private Book testingBook; + + @BeforeEach + void beforeEach() { + testingBook = new Book(); + testingBook.title = "Holy Bible"; + testingBook.outOfStock = true; + testingBook.publishYear = 1995; + testingBook.isbn13 = 9780310904168L; + testingBook.discount = 0.25; + testingBook.price = new BigDecimal("123.50"); + sessionFactoryScope.inTransaction(session -> session.persist(testingBook)); + + MONGO_COMMAND_LISTENER.clear(); + } + + @Test + void testBoolean() { + assertBookQuery( + "from Book where outOfStock = true", + "{'aggregate': 'books', 'pipeline': [{'$match': {'outOfStock': {'$eq': true}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}"); + } + + @Test + void testInteger() { + assertBookQuery( + "from Book where publishYear = 1995", + "{'aggregate': 'books', 'pipeline': [{'$match': {'publishYear': {'$eq': 1995}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}"); + } + + @Test + void testLong() { + assertBookQuery( + "from Book where isbn13 = 9780310904168L", + "{'aggregate': 'books', 'pipeline': [{'$match': {'isbn13': {'$eq': 9780310904168}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}"); + } + + @Test + void testDouble() { + assertBookQuery( + "from Book where discount = 0.25", + "{'aggregate': 'books', 'pipeline': [{'$match': {'discount': {'$eq': 0.25}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}"); + } + + @Test + void testString() { + assertBookQuery( + "from Book where title = 'Holy Bible'", + "{'aggregate': 'books', 'pipeline': [{'$match': {'title': {'$eq': 'Holy Bible'}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}"); + } + + @Test + void testBigDecimal() { + assertBookQuery( + "from Book where price = 123.50BD", + "{'aggregate': 'books', 'pipeline': [{'$match': {'price': {'$eq': {'$numberDecimal': '123.50'}}}}, {'$project': {'_id': true, 'discount': true, 'isbn13': true, 'outOfStock': true, 'price': true, 'publishYear': true, 'title': true}}]}"); + } + + private void assertBookQuery(String hql, String expectedMql) { + sessionFactoryScope.inTransaction(session -> { + var selectionQuery = session.createSelectionQuery(hql, Book.class); + var resultList = selectionQuery.getResultList(); + + var capturedCommands = MONGO_COMMAND_LISTENER.getFinishedCommands(); + + assertThat(capturedCommands) + .singleElement() + .extracting(TestCommandListener::getActualAggregateCommand) + .usingRecursiveComparison() + .isEqualTo(BsonDocument.parse(expectedMql)); + + assertThat(resultList).hasSize(1); + MongoTestAssertions.assertEquals(testingBook, resultList.get(0)); + }); + } + } + + @Entity(name = "Contact") + @Table(name = "contacts") + static class Contact { + @Id + int id; + + String name; + int age; + String country; + + Contact() {} + + Contact(int id, String name, int age, Country country) { + this.id = id; + this.name = name; + this.age = age; + this.country = country.name(); + } + } + + enum Country { + USA, + CANADA + } + + @Entity(name = "Book") + @Table(name = "books") + static class Book { + @Id + @ObjectIdGenerator + ObjectId id; + + String title; + Boolean outOfStock; + Integer publishYear; + Long isbn13; + Double discount; + BigDecimal price; + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/util/TestCommandListener.java b/src/integrationTest/java/com/mongodb/hibernate/util/TestCommandListener.java new file mode 100644 index 00000000..52327df6 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/util/TestCommandListener.java @@ -0,0 +1,70 @@ +/* + * 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.util; + +import static com.mongodb.hibernate.internal.MongoAssertions.assertNotNull; + +import com.mongodb.event.CommandFailedEvent; +import com.mongodb.event.CommandListener; +import com.mongodb.event.CommandStartedEvent; +import com.mongodb.event.CommandSucceededEvent; +import com.mongodb.hibernate.internal.MongoAssertions; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.bson.BsonDocument; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class TestCommandListener implements CommandListener { + + private final List finishedCommands = new ArrayList<>(); + + private final Map startedCommands = new HashMap<>(); + + @Override + public void commandStarted(CommandStartedEvent event) { + startedCommands.put(event.getOperationId(), event.getCommand().clone()); + } + + @Override + public void commandSucceeded(CommandSucceededEvent event) { + var startedCommand = assertNotNull(startedCommands.remove(event.getOperationId())); + finishedCommands.add(startedCommand); + } + + @Override + public void commandFailed(CommandFailedEvent event) { + throw MongoAssertions.fail(); + } + + public List getFinishedCommands() { + return List.copyOf(finishedCommands); + } + + public void clear() { + finishedCommands.clear(); + } + + public static BsonDocument getActualAggregateCommand(BsonDocument command) { + var actualCommand = new BsonDocument(); + actualCommand.put("aggregate", command.getString("aggregate")); + actualCommand.put("pipeline", command.getArray("pipeline")); + return actualCommand; + } +} 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 61f1d716..f53472f4 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -28,6 +28,11 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.FILTER; import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.PROJECT_STAGE_SPECIFICATIONS; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.GT; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.GTE; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.LT; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.LTE; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.NE; import static java.lang.String.format; import com.mongodb.hibernate.internal.FeatureNotSupportedException; @@ -35,8 +40,10 @@ import com.mongodb.hibernate.internal.translate.mongoast.AstDocument; import com.mongodb.hibernate.internal.translate.mongoast.AstElement; import com.mongodb.hibernate.internal.translate.mongoast.AstFieldUpdate; +import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; import com.mongodb.hibernate.internal.translate.mongoast.AstNode; import com.mongodb.hibernate.internal.translate.mongoast.AstParameterMarker; +import com.mongodb.hibernate.internal.translate.mongoast.AstValue; 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; @@ -46,26 +53,40 @@ import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStage; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageIncludeSpecification; import com.mongodb.hibernate.internal.translate.mongoast.command.aggregate.AstProjectStageSpecification; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstAndFilter; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFieldOperationFilter; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilter; import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilterFieldPath; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilterOperation; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstNorFilter; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstOrFilter; import java.io.IOException; import java.io.StringWriter; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import org.bson.BsonBoolean; +import org.bson.BsonDecimal128; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonString; +import org.bson.BsonValue; import org.bson.json.JsonMode; import org.bson.json.JsonWriter; import org.bson.json.JsonWriterSettings; +import org.bson.types.Decimal128; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; import org.hibernate.query.sqm.tree.expression.Conversion; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; @@ -88,6 +109,7 @@ 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.Expression; import org.hibernate.sql.ast.tree.expression.ExtractUnit; import org.hibernate.sql.ast.tree.expression.Format; import org.hibernate.sql.ast.tree.expression.JdbcLiteral; @@ -127,6 +149,7 @@ 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.Predicate; import org.hibernate.sql.ast.tree.predicate.SelfRenderingPredicate; import org.hibernate.sql.ast.tree.predicate.ThruthnessPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; @@ -139,6 +162,7 @@ import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.ast.AbstractRestrictedTableMutation; import org.hibernate.sql.model.ast.ColumnWriteFragment; @@ -149,6 +173,7 @@ 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.Nullable; abstract class AbstractMqlTranslator implements SqlAstTranslator { private static final JsonWriterSettings JSON_WRITER_SETTINGS = @@ -379,22 +404,49 @@ public void visitNamedTableReference(NamedTableReference namedTableReference) { @Override public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) { - var astComparisonFilterOperator = getAstComparisonFilterOperator(comparisonPredicate.getOperator()); + boolean isFieldInLeftHandSide = isFieldPathExpression(comparisonPredicate.getLeftHandExpression()); + boolean isFieldInRightHandSide = isFieldPathExpression(comparisonPredicate.getRightHandExpression()); + + if (isFieldInLeftHandSide == isFieldInRightHandSide) { + if (isFieldInLeftHandSide) { + throw new FeatureNotSupportedException("Currently comparison between two fields not supported"); + } else { + throw new FeatureNotSupportedException("Currently comparison between two values not supported"); + } + } + + String fieldPath; + AstValue comparisonValue; + + if (isFieldInLeftHandSide) { + fieldPath = acceptAndYield(comparisonPredicate.getLeftHandExpression(), FIELD_PATH); + comparisonValue = acceptAndYield(comparisonPredicate.getRightHandExpression(), FIELD_VALUE); + } else { + fieldPath = acceptAndYield(comparisonPredicate.getRightHandExpression(), FIELD_PATH); + comparisonValue = acceptAndYield(comparisonPredicate.getLeftHandExpression(), FIELD_VALUE); + } - var fieldPath = acceptAndYield(comparisonPredicate.getLeftHandExpression(), FIELD_PATH); - var fieldValue = acceptAndYield(comparisonPredicate.getRightHandExpression(), FIELD_VALUE); + var operator = isFieldInLeftHandSide + ? comparisonPredicate.getOperator() + : comparisonPredicate.getOperator().invert(); + var astComparisonFilterOperator = getAstComparisonFilterOperator(operator); - var filter = new AstFieldOperationFilter( - new AstFilterFieldPath(fieldPath), - new AstComparisonFilterOperation(astComparisonFilterOperator, fieldValue)); + AstFilterOperation astFilterOperation = + new AstComparisonFilterOperation(astComparisonFilterOperator, comparisonValue); + var filter = new AstFieldOperationFilter(new AstFilterFieldPath(fieldPath), astFilterOperation); astVisitorValueHolder.yield(FILTER, filter); } - private static AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) { - return switch (operator) { - case EQUAL -> EQ; - default -> throw new FeatureNotSupportedException("Unsupported operator: " + operator.name()); - }; + @Override + public void visitNegatedPredicate(NegatedPredicate negatedPredicate) { + var filter = acceptAndYield(negatedPredicate.getPredicate(), FILTER); + astVisitorValueHolder.yield(FILTER, new AstNorFilter(filter)); + } + + @Override + public void visitGroupedPredicate(GroupedPredicate groupedPredicate) { + var filter = acceptAndYield(groupedPredicate.getSubPredicate(), FILTER); + astVisitorValueHolder.yield(FILTER, filter); } @Override @@ -426,6 +478,32 @@ public void visitColumnReference(ColumnReference columnReference) { astVisitorValueHolder.yield(FIELD_PATH, columnReference.getColumnExpression()); } + @Override + public void visitQueryLiteral(QueryLiteral queryLiteral) { + var bsonValue = toBsonValue(queryLiteral.getLiteralValue()); + astVisitorValueHolder.yield(FIELD_VALUE, new AstLiteralValue(bsonValue)); + } + + @Override + public void visitJunction(Junction junction) { + var subFilters = new ArrayList(junction.getPredicates().size()); + for (Predicate predicate : junction.getPredicates()) { + subFilters.add(acceptAndYield(predicate, FILTER)); + } + var junctionFilter = + switch (junction.getNature()) { + case DISJUNCTION -> new AstOrFilter(subFilters); + case CONJUNCTION -> new AstAndFilter(subFilters); + }; + astVisitorValueHolder.yield(FILTER, junctionFilter); + } + + @Override + public void visitUnparsedNumericLiteral(UnparsedNumericLiteral unparsedNumericLiteral) { + astVisitorValueHolder.yield( + FIELD_VALUE, new AstLiteralValue(toBsonValue(unparsedNumericLiteral.getLiteralValue()))); + } + @Override public void visitDeleteStatement(DeleteStatement deleteStatement) { throw new FeatureNotSupportedException("TODO-HIBERNATE-46 https://jira.mongodb.org/browse/HIBERNATE-46"); @@ -611,16 +689,6 @@ public void visitJdbcLiteral(JdbcLiteral jdbcLiteral) { throw new FeatureNotSupportedException(); } - @Override - public void visitQueryLiteral(QueryLiteral queryLiteral) { - throw new FeatureNotSupportedException(); - } - - @Override - public void visitUnparsedNumericLiteral(UnparsedNumericLiteral unparsedNumericLiteral) { - throw new FeatureNotSupportedException(); - } - @Override public void visitUnaryOperationExpression(UnaryOperation unaryOperation) { throw new FeatureNotSupportedException(); @@ -656,11 +724,6 @@ public void visitSqlFragmentPredicate(SqlFragmentPredicate sqlFragmentPredicate) throw new FeatureNotSupportedException(); } - @Override - public void visitGroupedPredicate(GroupedPredicate groupedPredicate) { - throw new FeatureNotSupportedException(); - } - @Override public void visitInListPredicate(InListPredicate inListPredicate) { throw new FeatureNotSupportedException(); @@ -681,21 +744,11 @@ public void visitExistsPredicate(ExistsPredicate existsPredicate) { throw new FeatureNotSupportedException(); } - @Override - public void visitJunction(Junction junction) { - throw new FeatureNotSupportedException(); - } - @Override public void visitLikePredicate(LikePredicate likePredicate) { throw new FeatureNotSupportedException(); } - @Override - public void visitNegatedPredicate(NegatedPredicate negatedPredicate) { - throw new FeatureNotSupportedException(); - } - @Override public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) { throw new FeatureNotSupportedException(); @@ -746,6 +799,17 @@ public void visitCustomTableUpdate(TableUpdateCustomSql tableUpdateCustomSql) { throw new FeatureNotSupportedException(); } + void checkJdbcParameterBindingsSupportability(@Nullable JdbcParameterBindings jdbcParameterBindings) { + if (jdbcParameterBindings != null) { + for (var jdbcParameterBinding : jdbcParameterBindings.getBindings()) { + if (jdbcParameterBinding.getBindValue() == null) { + throw new FeatureNotSupportedException( + "TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74"); + } + } + } + } + void checkQueryOptionsSupportability(QueryOptions queryOptions) { if (queryOptions.getTimeout() != null) { throw new FeatureNotSupportedException("'timeout' inQueryOptions not supported"); @@ -756,7 +820,8 @@ void checkQueryOptionsSupportability(QueryOptions queryOptions) { if (Boolean.TRUE.equals(queryOptions.isReadOnly())) { throw new FeatureNotSupportedException("'readOnly' in QueryOptions not supported"); } - if (queryOptions.getAppliedGraph() != null) { + if (queryOptions.getAppliedGraph() != null + && queryOptions.getAppliedGraph().getGraph() != null) { throw new FeatureNotSupportedException("'appliedGraph' in QueryOptions not supported"); } if (queryOptions.getTupleTransformer() != null) { @@ -783,9 +848,6 @@ void checkQueryOptionsSupportability(QueryOptions queryOptions) { && !queryOptions.getLockOptions().isEmpty()) { throw new FeatureNotSupportedException("'lockOptions' in QueryOptions not supported"); } - if (queryOptions.getComment() != null) { - throw new FeatureNotSupportedException("TODO-HIBERNATE-53 https://jira.mongodb.org/browse/HIBERNATE-53"); - } if (queryOptions.getDatabaseHints() != null && !queryOptions.getDatabaseHints().isEmpty()) { throw new FeatureNotSupportedException("'databaseHints' in QueryOptions not supported"); @@ -797,4 +859,45 @@ void checkQueryOptionsSupportability(QueryOptions queryOptions) { throw new FeatureNotSupportedException("TODO-HIBERNATE-70 https://jira.mongodb.org/browse/HIBERNATE-70"); } } + + private static AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) { + return switch (operator) { + case EQUAL -> EQ; + case NOT_EQUAL -> NE; + case LESS_THAN -> LT; + case LESS_THAN_OR_EQUAL -> LTE; + case GREATER_THAN -> GT; + case GREATER_THAN_OR_EQUAL -> GTE; + default -> throw new FeatureNotSupportedException("Unsupported comparison operator: " + operator.name()); + }; + } + + private static boolean isFieldPathExpression(Expression expression) { + return expression instanceof ColumnReference || expression instanceof BasicValuedPathInterpretation; + } + + private static BsonValue toBsonValue(@Nullable Object queryLiteral) { + if (queryLiteral == null) { + throw new FeatureNotSupportedException("TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74"); + } + if (queryLiteral instanceof Boolean boolValue) { + return BsonBoolean.valueOf(boolValue); + } + if (queryLiteral instanceof Integer intValue) { + return new BsonInt32(intValue); + } + if (queryLiteral instanceof Long longValue) { + return new BsonInt64(longValue); + } + if (queryLiteral instanceof Double doubleValue) { + return new BsonDouble(doubleValue); + } + if (queryLiteral instanceof BigDecimal bigDecimalValue) { + return new BsonDecimal128(new Decimal128(bigDecimalValue)); + } + if (queryLiteral instanceof String stringValue) { + return new BsonString(stringValue); + } + throw new FeatureNotSupportedException("Unsupported Java type: " + queryLiteral.getClass()); + } } diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java index dfa6e131..3a745ac2 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java @@ -19,7 +19,6 @@ import static com.mongodb.hibernate.internal.translate.AstVisitorValueDescriptor.COLLECTION_AGGREGATE; import static org.hibernate.sql.ast.SqlTreePrinter.logSqlAst; -import com.mongodb.hibernate.internal.FeatureNotSupportedException; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.Statement; @@ -47,9 +46,7 @@ public JdbcOperationQuerySelect translate( logSqlAst(selectStatement); - if (jdbcParameterBindings != null) { - throw new FeatureNotSupportedException(); - } + checkJdbcParameterBindingsSupportability(jdbcParameterBindings); checkQueryOptionsSupportability(queryOptions); var aggregateCommand = acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AbstractAstLogicalFilter.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AbstractAstLogicalFilter.java new file mode 100644 index 00000000..5c04f262 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AbstractAstLogicalFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import static com.mongodb.hibernate.internal.MongoAssertions.assertFalse; + +import java.util.List; +import org.bson.BsonWriter; + +abstract class AbstractAstLogicalFilter implements AstFilter { + private final String operator; + private final List filters; + + AbstractAstLogicalFilter(String operator, List filters) { + assertFalse(filters.isEmpty()); + this.operator = operator; + this.filters = filters; + } + + @Override + public void render(BsonWriter writer) { + writer.writeStartDocument(); + { + writer.writeName(operator); + writer.writeStartArray(); + { + filters.forEach(filter -> filter.render(writer)); + } + writer.writeEndArray(); + } + writer.writeEndDocument(); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstAndFilter.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstAndFilter.java new file mode 100644 index 00000000..d8ba65d3 --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstAndFilter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import java.util.List; + +public final class AstAndFilter extends AbstractAstLogicalFilter { + public AstAndFilter(List filters) { + super("$and", filters); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperation.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperation.java index 83341d27..859c379e 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperation.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperation.java @@ -19,8 +19,16 @@ import com.mongodb.hibernate.internal.translate.mongoast.AstValue; import org.bson.BsonWriter; -public record AstComparisonFilterOperation(AstComparisonFilterOperator operator, AstValue value) - implements AstFilterOperation { +public class AstComparisonFilterOperation implements AstFilterOperation { + + private final AstComparisonFilterOperator operator; + private final AstValue value; + + public AstComparisonFilterOperation(AstComparisonFilterOperator operator, AstValue value) { + this.operator = operator; + this.value = value; + } + @Override public void render(BsonWriter writer) { writer.writeStartDocument(); diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperator.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperator.java index b74ba86c..01f6c5d9 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperator.java @@ -17,7 +17,12 @@ package com.mongodb.hibernate.internal.translate.mongoast.filter; public enum AstComparisonFilterOperator { - EQ("$eq"); + EQ("$eq"), + GT("$gt"), + GTE("$gte"), + LT("$lt"), + LTE("$lte"), + NE("$ne"); AstComparisonFilterOperator(String operatorName) { this.operatorName = operatorName; diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFilterOperation.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFilterOperation.java index 825b7a7c..7f9abe30 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFilterOperation.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFilterOperation.java @@ -18,4 +18,4 @@ import com.mongodb.hibernate.internal.translate.mongoast.AstNode; -interface AstFilterOperation extends AstNode {} +public interface AstFilterOperation extends AstNode {} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstNorFilter.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstNorFilter.java new file mode 100644 index 00000000..1037191b --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstNorFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import static java.util.Collections.singletonList; + +import java.util.List; + +public class AstNorFilter extends AbstractAstLogicalFilter { + + public AstNorFilter(AstFilter filter) { + this(singletonList(filter)); + } + + public AstNorFilter(List filters) { + super("$nor", filters); + } +} diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstOrFilter.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstOrFilter.java new file mode 100644 index 00000000..3445a2ef --- /dev/null +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstOrFilter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import java.util.List; + +public final class AstOrFilter extends AbstractAstLogicalFilter { + public AstOrFilter(List filters) { + super("$or", filters); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstTestUtils.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstTestUtils.java new file mode 100644 index 00000000..41f9f37e --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/AstTestUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast; + +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperation; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFieldOperationFilter; +import com.mongodb.hibernate.internal.translate.mongoast.filter.AstFilterFieldPath; +import org.bson.BsonValue; + +public final class AstTestUtils { + + private AstTestUtils() {} + + public static AstFieldOperationFilter createFieldOperationFilter( + String fieldPath, AstComparisonFilterOperator operator, BsonValue value) { + var filterFieldPath = new AstFilterFieldPath(fieldPath); + var filterOperation = new AstComparisonFilterOperation(operator, new AstLiteralValue(value)); + return new AstFieldOperationFilter(filterFieldPath, filterOperation); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstAndFilterTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstAndFilterTests.java new file mode 100644 index 00000000..5b2cfbfb --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstAndFilterTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; +import static com.mongodb.hibernate.internal.translate.mongoast.AstTestUtils.createFieldOperationFilter; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; + +import java.util.List; +import org.bson.BsonInt32; +import org.bson.BsonString; +import org.junit.jupiter.api.Test; + +class AstAndFilterTests { + + @Test + void testRendering() { + var astAndFilter = new AstAndFilter(List.of( + createFieldOperationFilter("field1", EQ, new BsonInt32(1)), + createFieldOperationFilter("field2", EQ, new BsonString("1")))); + + var expectedJson = + """ + {"$and": [{"field1": {"$eq": 1}}, {"field2": {"$eq": "1"}}]}\ + """; + assertRender(expectedJson, astAndFilter); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperationTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperationTests.java index b4156e35..806c1f2e 100644 --- a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperationTests.java +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstComparisonFilterOperationTests.java @@ -17,22 +17,30 @@ package com.mongodb.hibernate.internal.translate.mongoast.filter; import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; -import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; import org.bson.BsonInt32; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class AstComparisonFilterOperationTests { - @Test - void testRendering() { - - var astComparisonFilterOperation = new AstComparisonFilterOperation(EQ, new AstLiteralValue(new BsonInt32(1))); + @ParameterizedTest + @CsvSource({ + "EQ,$eq", + "GT,$gt", + "GTE,$gte", + "LT,$lt", + "LTE,$lte", + "NE,$ne", + }) + void testRendering(String operatorName, String operatorRendered) { + var operator = AstComparisonFilterOperator.valueOf(operatorName); + var operation = new AstComparisonFilterOperation(operator, new AstLiteralValue(new BsonInt32(1))); var expectedJson = """ - {"$eq": 1}\ - """; - assertRender(expectedJson, astComparisonFilterOperation); + {"%s": 1}\ + """.formatted(operatorRendered); + assertRender(expectedJson, operation); } } diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstNorFilterTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstNorFilterTests.java new file mode 100644 index 00000000..57a423f7 --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstNorFilterTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; + +import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; +import java.util.List; +import org.bson.BsonInt32; +import org.junit.jupiter.api.Test; + +class AstNorFilterTests { + @Test + void testRendering() { + var filters = List.of( + new AstFieldOperationFilter( + new AstFilterFieldPath("field1"), + new AstComparisonFilterOperation( + AstComparisonFilterOperator.EQ, new AstLiteralValue(new BsonInt32(1)))), + new AstFieldOperationFilter( + new AstFilterFieldPath("field2"), + new AstComparisonFilterOperation( + AstComparisonFilterOperator.NE, new AstLiteralValue(new BsonInt32(0))))); + var norFilter = new AstNorFilter(filters); + var expectedJson = + """ + {"$nor": [{"field1": {"$eq": 1}}, {"field2": {"$ne": 0}}]}\ + """; + assertRender(expectedJson, norFilter); + } +} diff --git a/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstOrFilterTests.java b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstOrFilterTests.java new file mode 100644 index 00000000..408e017e --- /dev/null +++ b/src/test/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstOrFilterTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.hibernate.internal.translate.mongoast.filter; + +import static com.mongodb.hibernate.internal.translate.mongoast.AstNodeAssertions.assertRender; +import static com.mongodb.hibernate.internal.translate.mongoast.AstTestUtils.createFieldOperationFilter; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.EQ; + +import java.util.List; +import org.bson.BsonInt32; +import org.bson.BsonString; +import org.junit.jupiter.api.Test; + +class AstOrFilterTests { + @Test + void testRendering() { + var astOrFilter = new AstOrFilter(List.of( + createFieldOperationFilter("field1", EQ, new BsonInt32(1)), + createFieldOperationFilter("field2", EQ, new BsonString("1")))); + + var expectedJson = + """ + {"$or": [{"field1": {"$eq": 1}}, {"field2": {"$eq": "1"}}]}\ + """; + assertRender(expectedJson, astOrFilter); + } +}