Skip to content
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

MongoDB Panache - Consistently encode list with [] #42699

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/src/main/asciidoc/mongodb-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -572,15 +572,17 @@
- `amount > ?1 and firstname != ?2` will be mapped to `{'amount': {'$gt': ?1}, 'firstname': {'$ne': ?2}}`
- `lastname like ?1` will be mapped to `{'lastname': {'$regex': ?1}}`. Be careful that this will be link:https://docs.mongodb.com/manual/reference/operator/query/regex/#op._S_regex[MongoDB regex] support and not SQL like pattern.
- `lastname is not null` will be mapped to `{'lastname':{'$exists': true}}`
- `status in ?1` will be mapped to `{'status':{$in: [?1]}}`
- `status in ?1` will be mapped to `{'status':{$in: ?1}}`

WARNING: MongoDB queries must be valid JSON documents,
using the same field multiple times in a query is not allowed using PanacheQL as it would generate an invalid JSON

Check warning on line 578 in docs/src/main/asciidoc/mongodb-panache.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/mongodb-panache.adoc", "range": {"start": {"line": 578, "column": 68}}}, "severity": "INFO"}
(see link:https://github.com/quarkusio/quarkus/issues/12086[this issue on GitHub]).

WARNING: Prior to Quarkus 3.16, when using `$in` with a list, you had to write your query with `{'status':{$in: [?1]}}`. Starting with Quarkus 3.16, make sure you use `{'status':{$in: ?1}}` instead. The list will be properly expanded with surrounding square brackets.

We also handle some basic date type transformations: all fields of type `Date`, `LocalDate`, `LocalDateTime` or `Instant` will be mapped to the
link:https://docs.mongodb.com/manual/reference/bson-types/#date[BSON Date] using the `ISODate` type (UTC datetime).

Check warning on line 584 in docs/src/main/asciidoc/mongodb-panache.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'datetime'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'datetime'?", "location": {"path": "docs/src/main/asciidoc/mongodb-panache.adoc", "range": {"start": {"line": 584, "column": 95}}}, "severity": "WARNING"}
The MongoDB POJO codec doesn't support `ZonedDateTime` and `OffsetDateTime` so you should convert them prior usage.

Check warning on line 585 in docs/src/main/asciidoc/mongodb-panache.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'codec'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'codec'?", "location": {"path": "docs/src/main/asciidoc/mongodb-panache.adoc", "range": {"start": {"line": 585, "column": 7}}}, "severity": "WARNING"}

MongoDB with Panache also supports extended MongoDB queries by providing a `Document` query, this is supported by the find/list/stream/count/delete/update methods.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ static String escape(Object value) {
return "null";
}
if (value.getClass().isArray() || Collection.class.isAssignableFrom(value.getClass())) {
return arrayAsString(value);
return "[" + arrayAsString(value) + "]";
}
if (Number.class.isAssignableFrom(value.getClass()) || value instanceof Boolean) {
return value.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,9 @@ private String unquote(String text) {
@Override
public String visitInPredicate(HqlParser.InPredicateContext ctx) {
StringBuilder sb = new StringBuilder(ctx.expression().accept(this))
.append(":{'$in':[")
.append(":{'$in':")
.append(ctx.inList().accept(this))
.append("]}");
.append("}");
return sb.toString();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ protected Object createQuery(MongoCollection collection, ClientSession session,

private static class DemoObj {
public String field;
public List<String> listField;
public boolean isOk;
@BsonProperty("value")
public String property;
Expand Down Expand Up @@ -146,19 +147,19 @@ public void testBindNativeFilterByIndex() {

//queries related to '$in' operator
List<Object> list = Arrays.asList("f1", "f2");
query = operations.bindFilter(DemoObj.class, "{ field: { '$in': [?1] } }", new Object[] { list });
query = operations.bindFilter(DemoObj.class, "{ field: { '$in': ?1 } }", new Object[] { list });
assertEquals("{ field: { '$in': ['f1', 'f2'] } }", query);

query = operations.bindFilter(DemoObj.class, "{ field: { '$in': [?1] }, isOk: ?2 }", new Object[] { list, true });
query = operations.bindFilter(DemoObj.class, "{ field: { '$in': ?1 }, isOk: ?2 }", new Object[] { list, true });
assertEquals("{ field: { '$in': ['f1', 'f2'] }, isOk: true }", query);

query = operations.bindFilter(DemoObj.class,
"{ field: { '$in': [?1] }, $or: [ {'property': ?2}, {'property': ?3} ] }",
"{ field: { '$in': ?1 }, $or: [ {'property': ?2}, {'property': ?3} ] }",
new Object[] { list, "jpg", "gif" });
assertEquals("{ field: { '$in': ['f1', 'f2'] }, $or: [ {'property': 'jpg'}, {'property': 'gif'} ] }", query);

query = operations.bindFilter(DemoObj.class,
"{ field: { '$in': [?1] }, isOk: ?2, $or: [ {'property': ?3}, {'property': ?4} ] }",
"{ field: { '$in': ?1 }, isOk: ?2, $or: [ {'property': ?3}, {'property': ?4} ] }",
new Object[] { list, true, "jpg", "gif" });
assertEquals("{ field: { '$in': ['f1', 'f2'] }, isOk: true, $or: [ {'property': 'jpg'}, {'property': 'gif'} ] }",
query);
Expand Down Expand Up @@ -205,21 +206,21 @@ public void testBindNativeFilterByName() {

//queries related to '$in' operator
List<Object> ids = Arrays.asList("f1", "f2");
query = operations.bindFilter(DemoObj.class, "{ field: { '$in': [:fields] } }",
query = operations.bindFilter(DemoObj.class, "{ field: { '$in': :fields } }",
Parameters.with("fields", ids).map());
assertEquals("{ field: { '$in': ['f1', 'f2'] } }", query);

query = operations.bindFilter(DemoObj.class, "{ field: { '$in': [:fields] }, isOk: :isOk }",
query = operations.bindFilter(DemoObj.class, "{ field: { '$in': :fields }, isOk: :isOk }",
Parameters.with("fields", ids).and("isOk", true).map());
assertEquals("{ field: { '$in': ['f1', 'f2'] }, isOk: true }", query);

query = operations.bindFilter(DemoObj.class,
"{ field: { '$in': [:fields] }, $or: [ {'property': :p1}, {'property': :p2} ] }",
"{ field: { '$in': :fields }, $or: [ {'property': :p1}, {'property': :p2} ] }",
Parameters.with("fields", ids).and("p1", "jpg").and("p2", "gif").map());
assertEquals("{ field: { '$in': ['f1', 'f2'] }, $or: [ {'property': 'jpg'}, {'property': 'gif'} ] }", query);

query = operations.bindFilter(DemoObj.class,
"{ field: { '$in': [:fields] }, isOk: :isOk, $or: [ {'property': :p1}, {'property': :p2} ] }",
"{ field: { '$in': :fields }, isOk: :isOk, $or: [ {'property': :p1}, {'property': :p2} ] }",
Parameters.with("fields", ids)
.and("isOk", true)
.and("p1", "jpg")
Expand Down Expand Up @@ -388,6 +389,10 @@ public void testBindUpdate() {
String update = operations.bindUpdate(DemoObj.class, "{'field': ?1}", new Object[] { "a value" });
assertEquals("{'$set':{'field': 'a value'}}", update);

// native update by index without $set
update = operations.bindUpdate(DemoObj.class, "{'listField': ?1}", new Object[] { List.of("value1", "value2") });
assertEquals("{'$set':{'listField': ['value1', 'value2']}}", update);

// native update by name without $set
update = operations.bindUpdate(Object.class, "{'field': :field}",
Parameters.with("field", "a value").map());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ public BookEntity search2(@QueryParam("author") String author, @QueryParam("titl
Parameters.with("dateFrom", LocalDate.parse(dateFrom)).and("dateTo", LocalDate.parse(dateTo))).firstResult();
}

@PUT
@Path("/update-categories/{id}")
public Response updateCategories(@PathParam("id") String id) {
BookEntity.update("categories = ?1", List.of("novel", "fiction")).where("_id", new ObjectId(id));
return Response.accepted().build();
}

@DELETE
public void deleteAll() {
BookEntity.deleteAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ public Book search2(@QueryParam("author") String author, @QueryParam("title") St
Parameters.with("dateFrom", LocalDate.parse(dateFrom)).and("dateTo", LocalDate.parse(dateTo))).firstResult();
}

@PUT
@Path("/update-categories/{id}")
public Response updateCategories(@PathParam("id") String id) {
bookRepository.update("categories = ?1", List.of("novel", "fiction")).where("_id", new ObjectId(id));
return Response.accepted().build();
}

@DELETE
public void deleteAll() {
bookRepository.deleteAll();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,21 @@ private void callBookEndpoint(String endpoint) {
book = get(endpoint + "/optional/" + book.getId().toString()).as(BookDTO.class);
Assertions.assertNotNull(book);

// update categories list using HQL
response = RestAssured
.given()
.header("Content-Type", "application/json")
.put(endpoint + "/update-categories/" + book.getId())
.andReturn();
Assertions.assertEquals(202, response.statusCode());

//check that the title and categories have been updated and the transient description ignored
book = get(endpoint + "/" + book.getId().toString()).as(BookDTO.class);
Assertions.assertNotNull(book);
Assertions.assertEquals("Notre-Dame de Paris 2", book.getTitle());
Assertions.assertNull(book.getTransientDescription());
Assertions.assertEquals(List.of("novel", "fiction"), book.getCategories());

//delete a book
response = RestAssured
.given()
Expand Down