diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/Book.java b/src/integrationTest/java/com/mongodb/hibernate/query/Book.java index df6caece..38ba0ce7 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/Book.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/Book.java @@ -45,6 +45,20 @@ public Book(int id, String title, Integer publishYear, Boolean outOfStock) { this.outOfStock = outOfStock; } + public Book( + int id, + String title, + Integer publishYear, + Boolean outOfStock, + Long isbn13, + Double discount, + BigDecimal price) { + this(id, title, publishYear, outOfStock); + this.isbn13 = isbn13; + this.discount = discount; + this.price = price; + } + @Override public String toString() { return "Book{" + "id=" + id + '}'; diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java index 7e4f1faf..51f9d221 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/DeletionIntegrationTests.java @@ -59,9 +59,18 @@ void testDeletionWithNonZeroMutationCount() { { "limit": 0, "q": { - "title": { - "$eq": "War and Peace" - } + "$and": [ + { + "title": { + "$eq": "War and Peace" + } + }, + { + "title": { + "$ne": null + } + } + ] } } ] @@ -121,9 +130,18 @@ void testDeletionWithZeroMutationCount() { { "limit": 0, "q": { - "publishYear": { - "$lt": 1850 - } + "$and": [ + { + "publishYear": { + "$lt": 1850 + } + }, + { + "publishYear": { + "$ne": null + } + } + ] } } ] diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java index 8cf65b05..9b850aea 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/mutation/UpdatingIntegrationTests.java @@ -59,9 +59,18 @@ void testUpdateWithNonZeroMutationCount() { { "multi": true, "q": { - "title": { - "$eq": "War & Peace" - } + "$and": [ + { + "title": { + "$eq": "War & Peace" + } + }, + { + "title": { + "$ne": null + } + } + ] }, "u": { "$set": { @@ -151,9 +160,18 @@ void testUpdateWithZeroMutationCount() { { "multi": true, "q": { - "publishYear": { - "$lt": 1850 - } + "$and": [ + { + "publishYear": { + "$lt": 1850 + } + }, + { + "publishYear": { + "$ne": null + } + } + ] }, "u": { "$set": { 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 7ab8f0ff..f2a583b2 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/BooleanExpressionWhereClauseIntegrationTests.java @@ -63,9 +63,18 @@ void testBooleanFieldPathExpression(boolean negated) { "pipeline": [ { "$match": { - "outOfStock": { - "$eq": %s - } + "$and": [ + { + "outOfStock": { + "$eq": %s + } + }, + { + "outOfStock": { + "$ne": null + } + } + ] } }, { diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/NullnessPredicateTranslationIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/NullnessPredicateTranslationIntegrationTests.java new file mode 100644 index 00000000..29ea7f02 --- /dev/null +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/NullnessPredicateTranslationIntegrationTests.java @@ -0,0 +1,128 @@ +/* + * 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 com.mongodb.hibernate.query.AbstractQueryIntegrationTests; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.hibernate.testing.orm.junit.DomainModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@DomainModel(annotatedClasses = {NullnessPredicateTranslationIntegrationTests.Book.class}) +class NullnessPredicateTranslationIntegrationTests extends AbstractQueryIntegrationTests { + + @BeforeEach + void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); + } + + private static final List testingBooks = List.of( + new Book(1, "War and Peace", null), + new Book(2, null, null), + new Book(3, "The Brothers Karamazov", 1880), + new Book(4, "War and Peace", 1867), + new Book(5, null, null)); + + private static List getBooksByIds(int... ids) { + return Arrays.stream(ids) + .mapToObj(id -> testingBooks.stream() + .filter(c -> c.id == id) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("id does not exist: " + id))) + .toList(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testNonEmbeddable(boolean isNegated) { + assertSelectionQuery( + "from Book where title = 'War and Peace' and publishYear " + (isNegated ? "is not null" : "is null"), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ + { + "$match": { + "$and": [ + { + "$and": [ + { + "title": { + "$eq": "War and Peace" + } + }, + { + "title": { + "$ne": null + } + } + ] + }, + { + "publishYear": { + "%s": null + } + } + ] + } + }, + { + "$project": { + "_id": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNegated ? "$ne" : "$eq"), + isNegated ? getBooksByIds(4) : getBooksByIds(1), + Set.of("books")); + } + + @Entity(name = "Book") + @Table(name = "books") + static class Book { + @Id + int id; + + String title; + + Integer publishYear; + + public Book() {} + + public Book(int id, String title, Integer publishYear) { + this.id = id; + this.title = title; + this.publishYear = publishYear; + } + + @Override + public String toString() { + return "Book{" + "id=" + id + '}'; + } + } +} diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java index 47b87a13..09c39ca1 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SimpleSelectQueryIntegrationTests.java @@ -16,6 +16,8 @@ package com.mongodb.hibernate.query.select; +import static java.lang.String.format; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThatCode; import com.mongodb.hibernate.internal.FeatureNotSupportedException; @@ -52,7 +54,10 @@ void beforeEach() { 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)); + new Contact(5, "John", 25, Country.USA), + new Contact(6, "Alice", 40, Country.USA), + new Contact(7, "Eve", null, null), + new Contact(8, "Eve", null, Country.CANADA)); private static List getTestingContacts(int... ids) { return Arrays.stream(ids) @@ -65,7 +70,7 @@ private static List getTestingContacts(int... ids) { @ParameterizedTest @ValueSource(booleans = {true, false}) - void testComparisonByEq(boolean fieldAsLhs) { + void testComparisonByEqToNonNull(boolean fieldAsLhs) { assertSelectionQuery( "from Contact where " + (fieldAsLhs ? "country = :country" : ":country = country"), Contact.class, @@ -76,10 +81,19 @@ void testComparisonByEq(boolean fieldAsLhs) { "pipeline": [ { "$match": { - "country": { - "$eq": "USA" - } - } + "$and": [ + { + "country": { + "$eq": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] + } }, { "$project": { @@ -91,27 +105,36 @@ void testComparisonByEq(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 5), + getTestingContacts(1, 5, 6), Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void testComparisonByNe(boolean fieldAsLhs) { + void testComparisonByEqToNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where " + (fieldAsLhs ? "country != ?1" : "?1 != country"), + "from Contact where " + (fieldAsLhs ? "country = :country" : ":country = country"), Contact.class, - q -> q.setParameter(1, Country.USA.name()), + q -> q.setParameter("country", null), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "country": { - "$ne": "USA" - } - } + "$and": [ + { + "country": { + "$eq": null + } + }, + { + "country": { + "$ne": null + } + } + ] + } }, { "$project": { @@ -123,26 +146,35 @@ void testComparisonByNe(boolean fieldAsLhs) { } ] }""", - getTestingContacts(2, 3, 4), + emptyList(), Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void testComparisonByLt(boolean fieldAsLhs) { + void testComparisonByNeToNonNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where " + (fieldAsLhs ? "age < :age" : ":age > age"), + "from Contact where " + (fieldAsLhs ? "country != ?1" : "?1 != country"), Contact.class, - q -> q.setParameter("age", 35), + q -> q.setParameter(1, Country.USA.name()), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "age": { - "$lt": 35 - } + "$and": [ + { + "country": { + "$ne": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] } }, { @@ -155,26 +187,35 @@ void testComparisonByLt(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 3, 5), + getTestingContacts(2, 3, 4, 8), Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void testComparisonByLte(boolean fieldAsLhs) { + void testComparisonByNeToNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where " + (fieldAsLhs ? "age <= ?1" : "?1 >= age"), + "from Contact where " + (fieldAsLhs ? "country != ?1" : "?1 != country"), Contact.class, - q -> q.setParameter(1, 35), + q -> q.setParameter(1, null), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "age": { - "$lte": 35 - } + "$and": [ + { + "country": { + "$ne": null + } + }, + { + "country": { + "$ne": null + } + } + ] } }, { @@ -187,26 +228,35 @@ void testComparisonByLte(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 2, 3, 5), + getTestingContacts(1, 2, 3, 4, 5, 6, 8), Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void testComparisonByGt(boolean fieldAsLhs) { + void testComparisonByLtNonNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where " + (fieldAsLhs ? "age > :age" : ":age < age"), + "from Contact where " + (fieldAsLhs ? "age < :age" : ":age > age"), Contact.class, - q -> q.setParameter("age", 18), + q -> q.setParameter("age", 35), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "age": { - "$gt": 18 - } + "$and": [ + { + "age": { + "$lt": 35 + } + }, + { + "age": { + "$ne": null + } + } + ] } }, { @@ -219,26 +269,35 @@ void testComparisonByGt(boolean fieldAsLhs) { } ] }""", - getTestingContacts(2, 4, 5), + getTestingContacts(1, 3, 5), Set.of(Contact.COLLECTION_NAME)); } @ParameterizedTest @ValueSource(booleans = {true, false}) - void testComparisonByGte(boolean fieldAsLhs) { + void testComparisonByLtNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where " + (fieldAsLhs ? "age >= :age" : ":age <= age"), + "from Contact where " + (fieldAsLhs ? "age < :age" : ":age > age"), Contact.class, - q -> q.setParameter("age", 18), + q -> q.setParameter("age", null), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "age": { - "$gte": 18 - } + "$and": [ + { + "age": { + "$lt": null + } + }, + { + "age": { + "$ne": null + } + } + ] } }, { @@ -251,16 +310,17 @@ void testComparisonByGte(boolean fieldAsLhs) { } ] }""", - getTestingContacts(1, 2, 4, 5), + emptyList(), Set.of(Contact.COLLECTION_NAME)); } - @Test - void testAndFilter() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByLteNonNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where country = ?1 and age > ?2", + "from Contact where " + (fieldAsLhs ? "age <= ?1" : "?1 >= age"), Contact.class, - q -> q.setParameter(1, Country.CANADA.name()).setParameter(2, 18), + q -> q.setParameter(1, 35), """ { "aggregate": "contacts", @@ -269,13 +329,13 @@ void testAndFilter() { "$match": { "$and": [ { - "country": { - "$eq": "CANADA" + "age": { + "$lte": 35 } }, { "age": { - "$gt": 18 + "$ne": null } } ] @@ -291,31 +351,32 @@ void testAndFilter() { } ] }""", - getTestingContacts(2, 4), + getTestingContacts(1, 2, 3, 5), Set.of(Contact.COLLECTION_NAME)); } - @Test - void testOrFilter() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByLteNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where country = :country or age > :age", + "from Contact where " + (fieldAsLhs ? "age <= ?1" : "?1 >= age"), Contact.class, - q -> q.setParameter("country", Country.CANADA.name()).setParameter("age", 18), + q -> q.setParameter(1, null), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "$or": [ + "$and": [ { - "country": { - "$eq": "CANADA" + "age": { + "$lte": null } }, { "age": { - "$gt": 18 + "$ne": null } } ] @@ -331,15 +392,17 @@ void testOrFilter() { } ] }""", - getTestingContacts(2, 3, 4, 5), + emptyList(), Set.of(Contact.COLLECTION_NAME)); } - @Test - void testSingleNegation() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByGtNonNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where age > 18 and not (country = 'USA')", + "from Contact where " + (fieldAsLhs ? "age > :age" : ":age < age"), Contact.class, + q -> q.setParameter("age", 18), """ { "aggregate": "contacts", @@ -353,13 +416,9 @@ void testSingleNegation() { } }, { - "$nor": [ - { - "country": { - "$eq": "USA" - } - } - ] + "age": { + "$ne": null + } } ] } @@ -374,37 +433,33 @@ void testSingleNegation() { } ] }""", - getTestingContacts(2, 4), + getTestingContacts(2, 4, 5, 6), Set.of(Contact.COLLECTION_NAME)); } - @Test - void testSingleNegationWithAnd() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByGtNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where not (country = 'USA' and age > 18)", + "from Contact where " + (fieldAsLhs ? "age > :age" : ":age < age"), Contact.class, + q -> q.setParameter("age", null), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "$nor": [ + "$and": [ { - "$and": [ - { - "country": { - "$eq": "USA" - } - }, - { - "age": { - "$gt": { - "$numberInt": "18" - } - } - } - ] + "age": { + "$gt": null + } + }, + { + "age": { + "$ne": null + } } ] } @@ -419,37 +474,33 @@ void testSingleNegationWithAnd() { } ] }""", - getTestingContacts(1, 2, 3, 4), + emptyList(), Set.of(Contact.COLLECTION_NAME)); } - @Test - void testSingleNegationWithOr() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByGteNonNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where not (country = 'USA' or age > 18)", + "from Contact where " + (fieldAsLhs ? "age >= :age" : ":age <= age"), Contact.class, + q -> q.setParameter("age", 18), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "$nor": [ + "$and": [ { - "$or": [ - { - "country": { - "$eq": "USA" - } - }, - { - "age": { - "$gt": { - "$numberInt": "18" - } - } - } - ] + "age": { + "$gte": 18 + } + }, + { + "age": { + "$ne": null + } } ] } @@ -464,48 +515,33 @@ void testSingleNegationWithOr() { } ] }""", - getTestingContacts(3), + getTestingContacts(1, 2, 4, 5, 6), Set.of(Contact.COLLECTION_NAME)); } - @Test - void testSingleNegationWithAndOr() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testComparisonByGteNull(boolean fieldAsLhs) { assertSelectionQuery( - "from Contact where not (country = 'USA' and age > 18 or age < 25)", + "from Contact where " + (fieldAsLhs ? "age >= :age" : ":age <= age"), Contact.class, + q -> q.setParameter("age", null), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "$nor": [ + "$and": [ { - "$or": [ - { - "$and": [ - { - "country": { - "$eq": "USA" - } - }, - { - "age": { - "$gt": { - "$numberInt": "18" - } - } - } - ] - }, - { - "age": { - "$lt": { - "$numberInt": "25" - } - } - } - ] + "age": { + "$gte": null + } + }, + { + "age": { + "$ne": null + } } ] } @@ -520,15 +556,16 @@ void testSingleNegationWithAndOr() { } ] }""", - getTestingContacts(2, 4), + emptyList(), Set.of(Contact.COLLECTION_NAME)); } @Test - void testDoubleNegation() { + void testAndFilter() { assertSelectionQuery( - "from Contact where age > 18 and not ( not (country = 'USA') )", + "from Contact where country = ?1 and age > ?2", Contact.class, + q -> q.setParameter(1, Country.CANADA.name()).setParameter(2, 18), """ { "aggregate": "contacts", @@ -537,20 +574,30 @@ void testDoubleNegation() { "$match": { "$and": [ { - "age": { - "$gt": 18 - } + "$and": [ + { + "country": { + "$eq": "CANADA" + } + }, + { + "country": { + "$ne": null + } + } + ] }, { - "$nor": [ + "$and": [ { - "$nor": [ - { - "country": { - "$eq": "USA" - } - } - ] + "age": { + "$gt": 18 + } + }, + { + "age": { + "$ne": null + } } ] } @@ -567,38 +614,482 @@ void testDoubleNegation() { } ] }""", - getTestingContacts(5), + getTestingContacts(2, 4), Set.of(Contact.COLLECTION_NAME)); } @Test - void testProjectWithoutAlias() { + void testOrFilter() { assertSelectionQuery( - "select name, age from Contact where country = :country", - Object[].class, - q -> q.setParameter("country", Country.CANADA.name()), + "from Contact where country = :country or age > :age", + Contact.class, + q -> q.setParameter("country", Country.CANADA.name()).setParameter("age", 18), """ { "aggregate": "contacts", "pipeline": [ { "$match": { - "country": { - "$eq": "CANADA" - } - } - }, - { - "$project": { - "name": true, - "age": true - } - } - ] - }""", - List.of(new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78}), - Set.of(Contact.COLLECTION_NAME)); - } + "$or": [ + { + "$and": [ + { + "country": { + "$eq": "CANADA" + } + }, + { + "country": { + "$ne": null + } + } + ], + }, + { + "$and": [ + { + "age": { + "$gt": 18 + } + }, + { + "age": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(2, 3, 4, 5, 6, 8), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testSingleNegation() { + assertSelectionQuery( + "from Contact where age > 18 and not (country = 'USA')", + Contact.class, + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$and": [ + { + "$and": [ + { + "age": { + "$gt": { + "$numberInt": "18" + } + } + }, + { + "age": { + "$ne": null + } + } + ] + }, + { + "$and": [ + { + "country": { + "$ne": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(2, 4), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testSingleNegationWithAnd() { + assertSelectionQuery( + "from Contact where not (country = 'USA' and age > 18)", + Contact.class, + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$or": [ + { + "$and": [ + { + "country": { + "$ne": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] + }, + { + "$and": [ + { + "age": { + "$lte": { + "$numberInt": "18" + } + } + }, + { + "age": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(1, 2, 3, 4, 8), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testSingleNegationWithOr() { + assertSelectionQuery( + "from Contact where not (country = 'USA' or age > 18)", + Contact.class, + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$and": [ + { + "$and": [ + { + "country": { + "$ne": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] + }, + { + "$and": [ + { + "age": { + "$lte": { + "$numberInt": "18" + } + } + }, + { + "age": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(3), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testSingleNegationWithAndOr() { + assertSelectionQuery( + "from Contact where not (country = 'USA' and age > 18 or age < 25)", + Contact.class, + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$and": [ + { + "$or": [ + { + "$and": [ + { + "country": { + "$ne": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] + }, + { + "$and": [ + { + "age": { + "$lte": { + "$numberInt": "18" + } + } + }, + { + "age": { + "$ne": null + } + } + ] + } + ] + }, + { + "$and": [ + { + "age": { + "$gte": { + "$numberInt": "25" + } + } + }, + { + "age": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(2, 4), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testDoubleNegation() { + assertSelectionQuery( + "from Contact where age > 18 and not ( not (country = 'USA') )", + Contact.class, + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$and": [ + { + "$and": [ + { + "age": { + "$gt": { + "$numberInt": "18" + } + } + }, + { + "age": { + "$ne": null + } + } + ] + }, + { + "$and": [ + { + "country": { + "$eq": "USA" + } + }, + { + "country": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(5, 6), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testDoubleNegationWithOr() { + assertSelectionQuery( + "from Contact where not ( not ( (country = :country) OR (age = :age) ) )", + Contact.class, + q -> q.setParameter("country", "CANADA").setParameter("age", null), + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$or": [ + { + "$and": [ + { + "country": { + "$eq": "CANADA" + } + }, + { + "country": { + "$ne": null + } + } + ] + }, + { + "$and": [ + { + "age": { + "$eq": null + } + }, + { + "age": { + "$ne": null + } + } + ] + } + ] + } + }, + { + "$project": { + "_id": true, + "age": true, + "country": true, + "name": true + } + } + ] + }""", + getTestingContacts(2, 3, 4, 8), + Set.of(Contact.COLLECTION_NAME)); + } + + @Test + void testProjectWithoutAlias() { + assertSelectionQuery( + "select name, age from Contact where country = :country", + Object[].class, + q -> q.setParameter("country", Country.CANADA.name()), + """ + { + "aggregate": "contacts", + "pipeline": [ + { + "$match": { + "$and": [ + { + "country": { + "$eq": "CANADA" + } + }, + { + "country": { + "$ne": null + } + } + ] + } + }, + { + "$project": { + "name": true, + "age": true + } + } + ] + }""", + List.of( + new Object[] {"Mary", 35}, + new Object[] {"Dylan", 7}, + new Object[] {"Lucy", 78}, + new Object[] {"Eve", null}), + Set.of(Contact.COLLECTION_NAME)); + } @Test void testProjectUsingAlias() { @@ -612,9 +1103,18 @@ void testProjectUsingAlias() { "pipeline": [ { "$match": { - "country": { - "$eq": "CANADA" - } + "$and": [ + { + "country": { + "$eq": "CANADA" + } + }, + { + "country": { + "$ne": null + } + } + ] } }, { @@ -625,7 +1125,11 @@ void testProjectUsingAlias() { } ] }""", - List.of(new Object[] {"Mary", 35}, new Object[] {"Dylan", 7}, new Object[] {"Lucy", 78}), + List.of( + new Object[] {"Mary", 35}, + new Object[] {"Dylan", 7}, + new Object[] {"Lucy", 78}, + new Object[] {"Eve", null}), Set.of(Contact.COLLECTION_NAME)); } @@ -689,16 +1193,6 @@ void testComparisonBetweenParametersNotSupported() { "Only the following comparisons are supported: field vs literal, field vs parameter"); } - @Test - void testNullParameterNotSupported() { - assertSelectQueryFailure( - "from Contact where country = :country", - Contact.class, - q -> q.setParameter("country", null), - FeatureNotSupportedException.class, - "TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74"); - } - @Test void testQueryPlanCacheIsSupported() { getSessionFactoryScope().inTransaction(session -> assertThatCode( @@ -709,224 +1203,297 @@ void testQueryPlanCacheIsSupported() { } } - @Nested - class QueryLiteralTests { - - private Book testingBook; + private static final List testingBooks = List.of( + new Book(1, "War and Peace", null, true, null, 0.2, new BigDecimal("123.50")), + new Book(2, "Crime and Punishment", 1866, null), + new Book(3, "Anna Karenina", null, false, 9780310904168L, 0.8, null), + new Book(4, "The Brothers Karamazov", null, null, null, 0.7, null), + new Book(5, "War and Peace", 2025, false), + new Book(6, null, null, null)); - @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"); - getSessionFactoryScope().inTransaction(session -> session.persist(testingBook)); + private static List getBooksByIds(int... ids) { + return Arrays.stream(ids) + .mapToObj(id -> testingBooks.stream() + .filter(c -> c.id == id) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("id does not exist: " + id))) + .toList(); + } - getTestCommandListener().clear(); - } + @BeforeEach + void beforeEach() { + getSessionFactoryScope().inTransaction(session -> testingBooks.forEach(session::persist)); + getTestCommandListener().clear(); + } - @Test - void testBoolean() { - assertSelectionQuery( - "from Book where outOfStock = true", - Book.class, - """ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testBoolean(boolean isNullLiteral) { + var nonNullBooleanLiteralStr = "true"; + assertSelectionQuery( + format("from Book where outOfStock = %s", isNullLiteral ? null : nonNullBooleanLiteralStr), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ { - "aggregate": "books", - "pipeline": [ - { - "$match": { + "$match": { + "$and": [ + { "outOfStock": { - "$eq": true + "$eq": %s + } + }, + { + "outOfStock": { + "$ne": null } } - }, - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - }""", - List.of(testingBook), - Set.of(Book.COLLECTION_NAME)); - } + ] + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNullLiteral ? null : nonNullBooleanLiteralStr), + isNullLiteral ? emptyList() : getBooksByIds(1), + Set.of(Book.COLLECTION_NAME)); + } - @Test - void testInteger() { - assertSelectionQuery( - "from Book where publishYear = 1995", - Book.class, - """ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testInteger(boolean isNullLiteral) { + var nonNullIntegerLiteralStr = "1866"; + assertSelectionQuery( + format("from Book where publishYear = %s", isNullLiteral ? null : nonNullIntegerLiteralStr), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ { - "aggregate": "books", - "pipeline": [ - { - "$match": { + "$match": { + "$and": [ + { "publishYear": { - "$eq": 1995 + "$eq": %s + } + }, + { + "publishYear": { + "$ne": null } } - }, - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - }""", - List.of(testingBook), - Set.of(Book.COLLECTION_NAME)); - } + ] + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNullLiteral ? null : nonNullIntegerLiteralStr), + isNullLiteral ? emptyList() : getBooksByIds(2), + Set.of(Book.COLLECTION_NAME)); + } - @Test - void testLong() { - assertSelectionQuery( - "from Book where isbn13 = 9780310904168L", - Book.class, - """ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testLong(boolean isNullLiteral) { + var nonNullLongLiteralStr = "9780310904168"; + assertSelectionQuery( + format("from Book where isbn13 = %s", isNullLiteral ? null : (nonNullLongLiteralStr + "L")), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ { - "aggregate": "books", - "pipeline": [ - { - "$match": { + "$match": { + "$and": [ + { "isbn13": { - "$eq": 9780310904168 + "$eq": %s + } + }, + { + "isbn13": { + "$ne": null } } - }, - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - }""", - List.of(testingBook), - Set.of(Book.COLLECTION_NAME)); - } + ] + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNullLiteral ? null : nonNullLongLiteralStr), + isNullLiteral ? emptyList() : getBooksByIds(3), + Set.of(Book.COLLECTION_NAME)); + } - @Test - void testDouble() { - assertSelectionQuery( - "from Book where discount = 0.25D", - Book.class, - """ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testDouble(boolean isNullLiteral) { + var nonNullLiteralStr = "0.5"; + assertSelectionQuery( + format("from Book where discount > %s", isNullLiteral ? null : (nonNullLiteralStr + "D")), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ { - "aggregate": "books", - "pipeline": [ - { - "$match": { + "$match": { + "$and": [ + { "discount": { - "$eq": 0.25 + "$gt": %s + } + }, + { + "discount": { + "$ne": null } } - }, - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - }""", - List.of(testingBook), - Set.of(Book.COLLECTION_NAME)); - } + ] + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNullLiteral ? null : nonNullLiteralStr), + isNullLiteral ? emptyList() : getBooksByIds(3, 4), + Set.of(Book.COLLECTION_NAME)); + } - @Test - void testString() { - assertSelectionQuery( - "from Book where title = 'Holy Bible'", - Book.class, - """ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testString(boolean isNullLiteral) { + var nonNullLiteralStr = "War and Peace"; + assertSelectionQuery( + format("from Book where title = %s", isNullLiteral ? null : ("\"" + nonNullLiteralStr + "\"")), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ { - "aggregate": "books", - "pipeline": [ - { - "$match": { + "$match": { + "$and": [ + { "title": { - "$eq": "Holy Bible" + "$eq": %s + } + }, + { + "title": { + "$ne": null } } - }, - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - }""", - List.of(testingBook), - Set.of(Book.COLLECTION_NAME)); - } + ] + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNullLiteral ? null : ("\"" + nonNullLiteralStr + "\"")), + isNullLiteral ? emptyList() : getBooksByIds(1, 5), + Set.of(Book.COLLECTION_NAME)); + } - @Test - void testBigDecimal() { - assertSelectionQuery( - "from Book where price = 123.50BD", - Book.class, - """ + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testBigDecimal(boolean isNullLiteral) { + var nonNullLiteralStr = "123.50"; + assertSelectionQuery( + format("from Book where price = %s", isNullLiteral ? null : (nonNullLiteralStr + "BD")), + Book.class, + """ + { + "aggregate": "books", + "pipeline": [ { - "aggregate": "books", - "pipeline": [ - { - "$match": { + "$match": { + "$and": [ + { "price": { - "$eq": { - "$numberDecimal": "123.50" - } + "$eq": %s + } + }, + { + "price": { + "$ne": null } } - }, - { - "$project": { - "_id": true, - "discount": true, - "isbn13": true, - "outOfStock": true, - "price": true, - "publishYear": true, - "title": true - } - } - ] - }""", - List.of(testingBook), - Set.of(Book.COLLECTION_NAME)); - } + ] + } + }, + { + "$project": { + "_id": true, + "discount": true, + "isbn13": true, + "outOfStock": true, + "price": true, + "publishYear": true, + "title": true + } + } + ] + }""" + .formatted(isNullLiteral ? null : "{\"$numberDecimal\": \"123.50\"}"), + isNullLiteral ? emptyList() : getBooksByIds(1), + Set.of(Book.COLLECTION_NAME)); } @Entity(name = "Contact") @@ -939,16 +1506,21 @@ static class Contact { int id; String name; - int age; + Integer age; String country; Contact() {} - Contact(int id, String name, int age, Country country) { + Contact(int id, String name, Integer age, Country country) { this.id = id; this.name = name; this.age = age; - this.country = country.name(); + this.country = country == null ? null : country.name(); + } + + @Override + public String toString() { + return "Contact{" + "id=" + id + '}'; } } diff --git a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java index 080e71fc..523cf46f 100644 --- a/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java +++ b/src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java @@ -151,9 +151,18 @@ void testOrderByMultipleFieldsWithoutTies() { "pipeline": [ { "$match": { - "outOfStock": { - "$eq": false - } + "$and": [ + { + "outOfStock": { + "$eq": false + } + }, + { + "outOfStock": { + "$ne": null + } + } + ] } }, { 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 800408e9..0034ca8e 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java @@ -44,7 +44,6 @@ import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.LTE; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.NE; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.AND; -import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.NOR; import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.OR; import static java.lang.String.format; import static org.hibernate.query.sqm.FetchClauseType.ROWS_ONLY; @@ -207,6 +206,8 @@ abstract class AbstractMqlTranslator implements SqlAstT private @Nullable QueryOptionsLimit queryOptionsLimit; + private boolean negated; + AbstractMqlTranslator(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; assertNotNull(sessionFactory @@ -504,7 +505,9 @@ public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) { assertTrue(isFieldPathExpression(rhs)); } - var fieldPath = acceptAndYield((isFieldOnLeftHandSide ? lhs : rhs), FIELD_PATH); + var fieldExpression = isFieldOnLeftHandSide ? lhs : rhs; + var fieldPath = acceptAndYield(fieldExpression, FIELD_PATH); + var comparisonValue = acceptAndYield((isFieldOnLeftHandSide ? rhs : lhs), VALUE); var operator = isFieldOnLeftHandSide @@ -513,14 +516,17 @@ public void visitRelationalPredicate(ComparisonPredicate comparisonPredicate) { var astComparisonFilterOperator = getAstComparisonFilterOperator(operator); var astFilterOperation = new AstComparisonFilterOperation(astComparisonFilterOperator, comparisonValue); - var filter = new AstFieldOperationFilter(fieldPath, astFilterOperation); + + var filter = AstFieldOperationFilter.toNullExclusionFilter(fieldPath, astFilterOperation); astVisitorValueHolder.yield(FILTER, filter); } @Override public void visitNegatedPredicate(NegatedPredicate negatedPredicate) { + negated = !negated; var filter = acceptAndYield(negatedPredicate.getPredicate(), FILTER); - astVisitorValueHolder.yield(FILTER, new AstLogicalFilter(NOR, List.of(filter))); + negated = !negated; + astVisitorValueHolder.yield(FILTER, filter); } @Override @@ -570,10 +576,11 @@ public void visitJunction(Junction junction) { for (Predicate predicate : junction.getPredicates()) { subFilters.add(acceptAndYield(predicate, FILTER)); } + var junctionFilter = switch (junction.getNature()) { - case DISJUNCTION -> new AstLogicalFilter(OR, subFilters); - case CONJUNCTION -> new AstLogicalFilter(AND, subFilters); + case DISJUNCTION -> new AstLogicalFilter(negated ? AND : OR, subFilters); + case CONJUNCTION -> new AstLogicalFilter(negated ? OR : AND, subFilters); }; astVisitorValueHolder.yield(FILTER, junctionFilter); } @@ -592,7 +599,7 @@ public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanEx var fieldPath = acceptAndYield(booleanExpressionPredicate.getExpression(), FIELD_PATH); var astFilterOperation = new AstComparisonFilterOperation(EQ, booleanExpressionPredicate.isNegated() ? FALSE : TRUE); - var filter = new AstFieldOperationFilter(fieldPath, astFilterOperation); + var filter = AstFieldOperationFilter.toNullExclusionFilter(fieldPath, astFilterOperation); astVisitorValueHolder.yield(FILTER, filter); } @@ -656,6 +663,25 @@ public void visitTuple(SqlTuple sqlTuple) { astVisitorValueHolder.yield(TUPLE, expressions); } + @Override + public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) { + var expression = nullnessPredicate.getExpression(); + var isNegated = nullnessPredicate.isNegated(); + var filter = createNullnessFieldComparisonFilter(expression, isNegated); + astVisitorValueHolder.yield(FILTER, filter); + } + + private AstFieldOperationFilter createNullnessFieldComparisonFilter(Expression expression, boolean isNegated) { + if (!isFieldPathExpression(expression)) { + throw new FeatureNotSupportedException( + format("Nullness predicate on expression [%s] not of field path is not supported", expression)); + } + var fieldPath = acceptAndYield(expression, FIELD_PATH); + var comparisonOperator = isNegated ? NE : EQ; + return new AstFieldOperationFilter( + fieldPath, new AstComparisonFilterOperation(comparisonOperator, AstLiteralValue.NULL)); + } + @Override public void visitDeleteStatement(DeleteStatement deleteStatement) { checkMutationStatementSupportability(deleteStatement); @@ -951,11 +977,6 @@ public void visitLikePredicate(LikePredicate likePredicate) { throw new FeatureNotSupportedException(); } - @Override - public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) { - throw new FeatureNotSupportedException(); - } - @Override public void visitThruthnessPredicate(ThruthnessPredicate thruthnessPredicate) { throw new FeatureNotSupportedException(); @@ -1056,8 +1077,9 @@ private static void checkQueryOptionsSupportability(QueryOptions queryOptions) { } } - private static AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) { - return switch (operator) { + private AstComparisonFilterOperator getAstComparisonFilterOperator(ComparisonOperator operator) { + ComparisonOperator currentOperator = negated ? operator.negated() : operator; + return switch (currentOperator) { case EQUAL -> EQ; case NOT_EQUAL -> NE; case LESS_THAN -> LT; diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/AstLiteralValue.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/AstLiteralValue.java index 7c45ea9e..36a0a312 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/AstLiteralValue.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/AstLiteralValue.java @@ -17,6 +17,7 @@ package com.mongodb.hibernate.internal.translate.mongoast; import org.bson.BsonBoolean; +import org.bson.BsonNull; import org.bson.BsonValue; import org.bson.BsonWriter; import org.bson.codecs.BsonValueCodec; @@ -30,6 +31,7 @@ public record AstLiteralValue(BsonValue literalValue) implements AstValue { public static final AstLiteralValue TRUE = new AstLiteralValue(BsonBoolean.TRUE); public static final AstLiteralValue FALSE = new AstLiteralValue(BsonBoolean.FALSE); + public static final AstLiteralValue NULL = new AstLiteralValue(BsonNull.VALUE); @Override public void render(BsonWriter writer) { diff --git a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFieldOperationFilter.java b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFieldOperationFilter.java index 437bf126..5b4e240a 100644 --- a/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFieldOperationFilter.java +++ b/src/main/java/com/mongodb/hibernate/internal/translate/mongoast/filter/AstFieldOperationFilter.java @@ -16,9 +16,19 @@ package com.mongodb.hibernate.internal.translate.mongoast.filter; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstComparisonFilterOperator.NE; +import static com.mongodb.hibernate.internal.translate.mongoast.filter.AstLogicalFilterOperator.AND; + +import com.mongodb.hibernate.internal.translate.mongoast.AstLiteralValue; +import java.util.List; +import org.bson.BsonNull; import org.bson.BsonWriter; public record AstFieldOperationFilter(String fieldPath, AstFilterOperation filterOperation) implements AstFilter { + + private static final AstComparisonFilterOperation NULL_EXCLUSION_FILTER_OPERATION = + new AstComparisonFilterOperation(NE, new AstLiteralValue(BsonNull.VALUE)); + @Override public void render(BsonWriter writer) { writer.writeStartDocument(); @@ -28,4 +38,12 @@ public void render(BsonWriter writer) { } writer.writeEndDocument(); } + + public static AstFilter toNullExclusionFilter(String fieldPath, AstFilterOperation filterOperation) { + return new AstLogicalFilter( + AND, + List.of( + new AstFieldOperationFilter(fieldPath, filterOperation), + new AstFieldOperationFilter(fieldPath, NULL_EXCLUSION_FILTER_OPERATION))); + } } diff --git a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java index 074e6d7b..bfab16e4 100644 --- a/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java +++ b/src/main/java/com/mongodb/hibernate/jdbc/MongoPreparedStatement.java @@ -23,7 +23,6 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; -import com.mongodb.hibernate.internal.FeatureNotSupportedException; import com.mongodb.hibernate.internal.type.MongoStructJdbcType; import com.mongodb.hibernate.internal.type.ObjectIdJdbcType; import java.math.BigDecimal; @@ -41,7 +40,6 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.List; -import java.util.Set; import java.util.function.Consumer; import org.bson.BsonArray; import org.bson.BsonDocument; @@ -86,42 +84,8 @@ private void checkAllParametersSet() throws SQLException { throw new SQLException(format("Parameter with index [%d] is not set", i + 1)); } } - checkComparatorNotComparingWithNullValues(); } - /** - * Temporary method to ensure exception is thrown when comparison query operators are comparing with {@code null} - * values. - * - *

Note that only find expression is involved before HIBERNATE-74. TODO-HIBERNATE-74 delete this temporary method - */ - private void checkComparatorNotComparingWithNullValues() { - checkComparatorNotComparingWithNullValuesRecursively(command); - } - - private void checkComparatorNotComparingWithNullValuesRecursively(BsonDocument document) { - for (var entry : document.entrySet()) { - if (COMPARISON_OPERATOR_NAMES_SUPPORTED.contains(entry.getKey()) - && entry.getValue().isNull()) { - throw new FeatureNotSupportedException( - "TODO-HIBERNATE-74 https://jira.mongodb.org/browse/HIBERNATE-74"); - } - if (entry.getValue().isDocument()) { - checkComparatorNotComparingWithNullValuesRecursively( - entry.getValue().asDocument()); - } else if (entry.getValue().isArray()) { - for (var bsonValue : entry.getValue().asArray()) { - if (bsonValue.isDocument()) { - checkComparatorNotComparingWithNullValuesRecursively(bsonValue.asDocument()); - } - } - } - } - } - - private static final Set COMPARISON_OPERATOR_NAMES_SUPPORTED = - Set.of("$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$nin"); - @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { checkClosed();