-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
simple selection query implementation #81
base: main
Are you sure you want to change the base?
Changes from all commits
f9be38a
30c03b3
473da26
3fe235c
5225759
6a7620d
1dcc606
4580066
abd1def
37e5b4f
0cff56e
1ed3053
9218a16
e785be6
d97be0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
/* | ||
* 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.junit.MongoExtension; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Table; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.function.Consumer; | ||
import org.hibernate.engine.spi.SessionImplementor; | ||
import org.hibernate.query.SelectionQuery; | ||
import org.hibernate.testing.jdbc.SQLStatementInspector; | ||
import org.hibernate.testing.orm.junit.DomainModel; | ||
import org.hibernate.testing.orm.junit.SessionFactory; | ||
import org.hibernate.testing.orm.junit.SessionFactoryScope; | ||
import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; | ||
import org.jspecify.annotations.Nullable; | ||
import org.junit.jupiter.api.BeforeEach; | ||
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, useCollectingStatementInspector = true) | ||
@DomainModel(annotatedClasses = SimpleSelectQueryIntegrationTests.Contact.class) | ||
@ExtendWith(MongoExtension.class) | ||
class SimpleSelectQueryIntegrationTests implements SessionFactoryScopeAware { | ||
|
||
private SessionFactoryScope sessionFactoryScope; | ||
private SQLStatementInspector mqlStatementInterceptor; | ||
|
||
@Override | ||
public void injectSessionFactoryScope(SessionFactoryScope sessionFactoryScope) { | ||
this.sessionFactoryScope = sessionFactoryScope; | ||
mqlStatementInterceptor = sessionFactoryScope.getCollectingStatementInspector(); | ||
} | ||
|
||
private final List<Contact> 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<Contact> getTestingContacts(int... ids) { | ||
return Arrays.stream(ids).mapToObj(id -> testingContacts.get(id - 1)).toList(); | ||
} | ||
|
||
@BeforeEach | ||
void beforeEach() { | ||
sessionFactoryScope.inTransaction(session -> testingContacts.forEach(session::persist)); | ||
mqlStatementInterceptor.clear(); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void testComparisonByEq(boolean fieldAsLhs) { | ||
assertQuery( | ||
"from Contact where " + (fieldAsLhs ? "country = :country" : ":country = country"), | ||
q -> q.setParameter("country", Country.USA.name()), | ||
"{'aggregate': 'contacts', 'pipeline': [{'$match': {'country': {'$eq': {'$undefined': true}}}}, {'$project': {'_id': true, 'age': true, 'country': true, 'name': true}}]}", | ||
getTestingContacts(1, 5)); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void testComparisonByNe(boolean fieldAsLhs) { | ||
sessionFactoryScope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where " + (fieldAsLhs ? "country != ?1" : "?1 != country"), | ||
q -> q.setParameter(1, Country.USA.name()), | ||
List.of(2, 3, 4))); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void testComparisonByLt(boolean fieldAsLhs) { | ||
sessionFactoryScope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where " + (fieldAsLhs ? "age < :age" : ":age > age"), | ||
q -> q.setParameter("age", 35), | ||
List.of(1, 3, 5))); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void testComparisonByLte(boolean fieldAsLhs) { | ||
sessionFactoryScope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where " + (fieldAsLhs ? "age <= ?1" : "?1 >= age"), | ||
q -> q.setParameter(1, 35), | ||
List.of(1, 2, 3, 5))); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void testComparisonByGt(boolean fieldAsLhs) { | ||
sessionFactoryScope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where " + (fieldAsLhs ? "age > :age" : ":age < age"), | ||
q -> q.setParameter("age", 18), | ||
List.of(2, 4, 5))); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void testComparisonByGte(boolean fieldAsLhs) { | ||
sessionFactoryScope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where " + (fieldAsLhs ? "age >= :age" : ":age <= age"), | ||
q -> q.setParameter("age", 18), | ||
List.of(1, 2, 4, 5))); | ||
} | ||
|
||
@Test | ||
void testAndFilter(SessionFactoryScope scope) { | ||
scope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where country = ?1 and age > ?2", | ||
q -> q.setParameter(1, Country.CANADA.name()).setParameter(2, 18), | ||
List.of(2, 4))); | ||
} | ||
|
||
@Test | ||
void testOrFilter(SessionFactoryScope scope) { | ||
scope.inTransaction(session -> assertContactQueryResult( | ||
session, | ||
"from Contact where country = :country or age > :age", | ||
q -> q.setParameter("country", Country.CANADA.name()).setParameter("age", 18), | ||
List.of(2, 3, 4, 5))); | ||
} | ||
|
||
@Test | ||
void testSingleNegation(SessionFactoryScope scope) { | ||
scope.inTransaction(session -> assertContactQueryResult( | ||
session, "from Contact where age > 18 and not (country = 'USA')", null, List.of(2, 4))); | ||
} | ||
|
||
@Test | ||
void testDoubleNegation(SessionFactoryScope scope) { | ||
scope.inTransaction(session -> assertContactQueryResult( | ||
session, "from Contact where age > 18 and not ( not (country = 'USA') )", null, List.of(5))); | ||
} | ||
|
||
@Test | ||
void testProjectOperation(SessionFactoryScope scope) { | ||
scope.inTransaction(session -> { | ||
var results = session.createSelectionQuery( | ||
jyemin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"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}); | ||
}); | ||
} | ||
|
||
private static void assertContactQueryResult( | ||
SessionImplementor session, | ||
String hql, | ||
@Nullable Consumer<SelectionQuery<Contact>> queryPostProcessor, | ||
List<Integer> expectedIds) { | ||
var selectionQuery = session.createSelectionQuery(hql, Contact.class); | ||
if (queryPostProcessor != null) { | ||
queryPostProcessor.accept(selectionQuery); | ||
} | ||
var queryResult = selectionQuery.getResultList(); | ||
assertThat(queryResult).extracting(c -> c.id).containsExactly(expectedIds.toArray(new Integer[0])); | ||
} | ||
|
||
private void assertQuery( | ||
String hql, | ||
Consumer<SelectionQuery<Contact>> queryPostProcessor, | ||
String mql, | ||
List<Contact> expectedContacts) { | ||
sessionFactoryScope.inTransaction(session -> { | ||
var selectionQuery = session.createSelectionQuery(hql, Contact.class); | ||
if (queryPostProcessor != null) { | ||
queryPostProcessor.accept(selectionQuery); | ||
} | ||
var queryResult = selectionQuery.getResultList(); | ||
assertThat(mqlStatementInterceptor.getSqlQueries()).containsExactly(mql.replace('\'', '"')); | ||
assertThat(queryResult) | ||
.usingRecursiveFieldByFieldElementComparator() | ||
.containsExactly(expectedContacts.toArray(new Contact[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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/* | ||
* 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.annotations.ObjectIdGenerator; | ||
import com.mongodb.hibernate.junit.MongoExtension; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.Id; | ||
import jakarta.persistence.Table; | ||
import java.math.BigDecimal; | ||
import org.bson.types.ObjectId; | ||
import org.hibernate.testing.orm.junit.DomainModel; | ||
import org.hibernate.testing.orm.junit.SessionFactory; | ||
import org.hibernate.testing.orm.junit.SessionFactoryScope; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
|
||
@SessionFactory(exportSchema = false) | ||
@DomainModel(annotatedClasses = SimpleSelectQueryLiteralIntegrationTests.Item.class) | ||
@ExtendWith(MongoExtension.class) | ||
class SimpleSelectQueryLiteralIntegrationTests { | ||
|
||
@Test | ||
void testBoolean(SessionFactoryScope scope) { | ||
var item = new Item(); | ||
item.booleanField = true; | ||
scope.inTransaction(session -> session.persist(item)); | ||
scope.inTransaction( | ||
session -> assertThat(session.createSelectionQuery("from Item where booleanField = true", Item.class) | ||
.getSingleResult()) | ||
.usingRecursiveComparison() | ||
.isEqualTo(item)); | ||
Comment on lines
+41
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests seem to be the same as the tests above. That is, they could have been written as:
Is there any reason they are written differently in this class? It also seems they could all just be in the same file. (If there is no reason, let's avoid making any changes until the other comment regarding testing against mql is resolved.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no special reason. I created a separate testing class so we could focus on query literal exclusively, then I followed the same pattern focusing on the query literal concern alone. |
||
} | ||
|
||
@Test | ||
void testInteger(SessionFactoryScope scope) { | ||
var item = new Item(); | ||
item.integerField = 1; | ||
scope.inTransaction(session -> session.persist(item)); | ||
scope.inTransaction( | ||
session -> assertThat(session.createSelectionQuery("from Item where integerField = 1", Item.class) | ||
.getSingleResult()) | ||
.usingRecursiveComparison() | ||
.isEqualTo(item)); | ||
} | ||
|
||
@Test | ||
void testLong(SessionFactoryScope scope) { | ||
var item = new Item(); | ||
item.longField = 1L; | ||
scope.inTransaction(session -> session.persist(item)); | ||
scope.inTransaction( | ||
session -> assertThat(session.createSelectionQuery("from Item where longField = 1", Item.class) | ||
.getSingleResult()) | ||
.usingRecursiveComparison() | ||
.isEqualTo(item)); | ||
} | ||
|
||
@Test | ||
void testDouble(SessionFactoryScope scope) { | ||
var item = new Item(); | ||
item.doubleField = 3.14; | ||
scope.inTransaction(session -> session.persist(item)); | ||
scope.inTransaction( | ||
session -> assertThat(session.createSelectionQuery("from Item where doubleField = 3.14", Item.class) | ||
.getSingleResult()) | ||
.usingRecursiveComparison() | ||
.isEqualTo(item)); | ||
} | ||
|
||
@Test | ||
void testString(SessionFactoryScope scope) { | ||
var item = new Item(); | ||
item.stringField = "Hello World"; | ||
scope.inTransaction(session -> session.persist(item)); | ||
scope.inTransaction(session -> assertThat( | ||
session.createSelectionQuery("from Item where stringField = 'Hello World'", Item.class) | ||
.getSingleResult()) | ||
.usingRecursiveComparison() | ||
.isEqualTo(item)); | ||
} | ||
|
||
@Test | ||
void testBigDecimal(SessionFactoryScope scope) { | ||
var item = new Item(); | ||
item.bigDecimalField = new BigDecimal("3.14"); | ||
scope.inTransaction(session -> session.persist(item)); | ||
scope.inTransaction(session -> assertThat( | ||
session.createSelectionQuery("from Item where bigDecimalField = 3.14BD", Item.class) | ||
.getSingleResult()) | ||
.usingRecursiveComparison() | ||
.isEqualTo(item)); | ||
} | ||
|
||
@Entity(name = "Item") | ||
@Table(name = "items") | ||
static class Item { | ||
@Id | ||
@ObjectIdGenerator | ||
ObjectId id; | ||
|
||
String stringField; | ||
Boolean booleanField; | ||
Integer integerField; | ||
Long longField; | ||
Double doubleField; | ||
BigDecimal bigDecimalField; | ||
} | ||
Comment on lines
+113
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be testing parameterizations for all of these? I think that we should use the same entity for everything, so that it is familiar and we do not have to think of something different when opening another file. Let's use semantic names, like you did in the previous tests I reviewed. I liked those - for me, it made it surprisingly easier to understand what a query was doing when the parameters were realistic. I suggest the following:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This testing class or testing cases are focusing on one thing: query literal, so parameterizations is not in the scope. The semantic naming is fine, but remember the key part here is the literal data type so using field name containing types makes the HQL self-explanatory. For normal entity classes (like in our previous CRUD testing cases), I would choose semantic field names. Could we retain the current field names for I do think they are better to reflect our testing purpose? |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to intercept the final query sent to the database (with filled-in parameters)? That might be more suitable for an integration test and help verify that the query was constructed as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect it could be done by configuring the
MongoClientSettings
with aCommandListener
, similar to what we do in driver unified tests.