Skip to content

Commit

Permalink
Merge pull request quarkusio#38764 from FroMage/36496
Browse files Browse the repository at this point in the history
ORM/HR/Panache: support `with` and `where` queries
  • Loading branch information
gsmet committed Feb 14, 2024
2 parents bd90be2 + 898f8a7 commit 05b4c23
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 40 deletions.
9 changes: 5 additions & 4 deletions docs/src/main/asciidoc/hibernate-orm-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -746,22 +746,23 @@ The `Sort` class has plenty of methods for adding columns and specifying sort di
Normally, HQL queries are of this form: `from EntityName [where ...] [order by ...]`, with optional elements
at the end.

If your select query does not start with `from`, we support the following additional forms:
If your select query does not start with `from`, `select` or `with`, we support the following additional forms:

- `order by ...` which will expand to `from EntityName order by ...`
- `<singleColumnName>` (and single parameter) which will expand to `from EntityName where <singleColumnName> = ?`
- `<singleAttribute>` (and single parameter) which will expand to `from EntityName where <singleAttribute> = ?`
- `where <query>` will expand to `from EntityName where <query>`
- `<query>` will expand to `from EntityName where <query>`

If your update query does not start with `update`, we support the following additional forms:

- `from EntityName ...` which will expand to `update EntityName ...`
- `set? <singleColumnName>` (and single parameter) which will expand to `update EntityName set <singleColumnName> = ?`
- `set? <singleAttribute>` (and single parameter) which will expand to `update EntityName set <singleAttribute> = ?`
- `set? <update-query>` will expand to `update EntityName set <update-query>`

If your delete query does not start with `delete`, we support the following additional forms:

- `from EntityName ...` which will expand to `delete from EntityName ...`
- `<singleColumnName>` (and single parameter) which will expand to `delete from EntityName where <singleColumnName> = ?`
- `<singleAttribute>` (and single parameter) which will expand to `delete from EntityName where <singleAttribute> = ?`
- `<query>` will expand to `delete from EntityName where <query>`

NOTE: You can also write your queries in plain
Expand Down
13 changes: 7 additions & 6 deletions docs/src/main/asciidoc/hibernate-reactive-panache.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -518,22 +518,23 @@ The `Sort` class has plenty of methods for adding columns and specifying sort di
Normally, HQL queries are of this form: `from EntityName [where ...] [order by ...]`, with optional elements
at the end.

If your select query does not start with `from`, we support the following additional forms:
If your select query does not start with `from`, `select` or `with`, we support the following additional forms:

- `order by ...` which will expand to `from EntityName order by ...`
- `<singleColumnName>` (and single parameter) which will expand to `from EntityName where <singleColumnName> = ?`
- `<singleAttribute>` (and single parameter) which will expand to `from EntityName where <singleAttribute> = ?`
- `where <query>` will expand to `from EntityName where <query>`
- `<query>` will expand to `from EntityName where <query>`

If your update query does not start with `update`, we support the following additional forms:

- `from EntityName ...` which will expand to `update from EntityName ...`
- `set? <singleColumnName>` (and single parameter) which will expand to `update from EntityName set <singleColumnName> = ?`
- `set? <update-query>` will expand to `update from EntityName set <update-query>`
- `from EntityName ...` which will expand to `update EntityName ...`
- `set? <singleAttribute>` (and single parameter) which will expand to `update EntityName set <singleAttribute> = ?`
- `set? <update-query>` will expand to `update EntityName set <update-query>`

If your delete query does not start with `delete`, we support the following additional forms:

- `from EntityName ...` which will expand to `delete from EntityName ...`
- `<singleColumnName>` (and single parameter) which will expand to `delete from EntityName where <singleColumnName> = ?`
- `<singleAttribute>` (and single parameter) which will expand to `delete from EntityName where <singleAttribute> = ?`
- `<query>` will expand to `delete from EntityName where <query>`

NOTE: You can also write your queries in plain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,31 +57,35 @@
* at the end.
* </p>
* <p>
* If your select query does not start with <code>from</code>, we support the following additional forms:
* If your select query does not start with <code>from</code>, <code>select</code>, or <code>with</code>, we support the
* following additional forms:
* </p>
* <ul>
* <li><code>order by ...</code> which will expand to <code>from EntityName order by ...</code></li>
* <li><code>&lt;singleColumnName&gt;</code> (and single parameter) which will expand to
* <code>from EntityName where &lt;singleColumnName&gt; = ?</code></li>
* <li><code>&lt;singleAttribute&gt;</code> (and single parameter) which will expand to
* <code>from EntityName where &lt;singleAttribute&gt; = ?</code></li>
* <li><code>where &lt;query&gt;</code> will expand to <code>from EntityName where &lt;query&gt;</code>
* <li><code>&lt;query&gt;</code> will expand to <code>from EntityName where &lt;query&gt;</code></li>
* </ul>
*
* <p>
* If your update query does not start with <code>update from</code>, we support the following additional forms:
* </p>
* <ul>
* <li><code>from EntityName ...</code> which will expand to <code>update from EntityName ...</code></li>
* <li><code>set? &lt;singleColumnName&gt;</code> (and single parameter) which will expand to
* <code>update from EntityName set &lt;singleColumnName&gt; = ?</code></li>
* <li><code>set? &lt;singleAttribute&gt;</code> (and single parameter) which will expand to
* <code>update from EntityName set &lt;singleAttribute&gt; = ?</code></li>
* <li><code>set? &lt;update-query&gt;</code> will expand to
* <code>update from EntityName set &lt;update-query&gt; = ?</code></li>
* </ul>
*
* <p>
* If your delete query does not start with <code>delete from</code>, we support the following additional forms:
* </p>
* <ul>
* <li><code>from EntityName ...</code> which will expand to <code>delete from EntityName ...</code></li>
* <li><code>&lt;singleColumnName&gt;</code> (and single parameter) which will expand to
* <code>delete from EntityName where &lt;singleColumnName&gt; = ?</code></li>
* <li><code>&lt;singleAttribute&gt;</code> (and single parameter) which will expand to
* <code>delete from EntityName where &lt;singleAttribute&gt; = ?</code></li>
* <li><code>&lt;query&gt;</code> will expand to <code>delete from EntityName where &lt;query&gt;</code></li>
* </ul>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,31 +57,35 @@
* at the end.
* </p>
* <p>
* If your select query does not start with <code>from</code>, we support the following additional forms:
* If your select query does not start with <code>from</code>, <code>select</code>, or <code>with</code>, we support the
* following additional forms:
* </p>
* <ul>
* <li><code>order by ...</code> which will expand to <code>from EntityName order by ...</code></li>
* <li><code>&lt;singleColumnName&gt;</code> (and single parameter) which will expand to
* <code>from EntityName where &lt;singleColumnName&gt; = ?</code></li>
* <li><code>&lt;singleAttribute&gt;</code> (and single parameter) which will expand to
* <code>from EntityName where &lt;singleAttribute&gt; = ?</code></li>
* <li><code>where &lt;query&gt;</code> will expand to <code>from EntityName where &lt;query&gt;</code>
* <li><code>&lt;query&gt;</code> will expand to <code>from EntityName where &lt;query&gt;</code></li>
* </ul>
*
* <p>
* If your update query does not start with <code>update from</code>, we support the following additional forms:
* </p>
* <ul>
* <li><code>from EntityName ...</code> which will expand to <code>update from EntityName ...</code></li>
* <li><code>set? &lt;singleColumnName&gt;</code> (and single parameter) which will expand to
* <code>update from EntityName set &lt;singleColumnName&gt; = ?</code></li>
* <li><code>set? &lt;singleAttribute&gt;</code> (and single parameter) which will expand to
* <code>update from EntityName set &lt;singleAttribute&gt; = ?</code></li>
* <li><code>set? &lt;update-query&gt;</code> will expand to
* <code>update from EntityName set &lt;update-query&gt; = ?</code></li>
* </ul>
*
* <p>
* If your delete query does not start with <code>delete from</code>, we support the following additional forms:
* </p>
* <ul>
* <li><code>from EntityName ...</code> which will expand to <code>delete from EntityName ...</code></li>
* <li><code>&lt;singleColumnName&gt;</code> (and single parameter) which will expand to
* <code>delete from EntityName where &lt;singleColumnName&gt; = ?</code></li>
* <li><code>&lt;singleAttribute&gt;</code> (and single parameter) which will expand to
* <code>delete from EntityName where &lt;singleAttribute&gt; = ?</code></li>
* <li><code>&lt;query&gt;</code> will expand to <code>delete from EntityName where &lt;query&gt;</code></li>
* </ul>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,24 @@ public String visitQuery(QueryContext ctx) {

@Override
public String visitSelectClause(SelectClauseContext ctx) {
if (ctx.SELECT() != null) {
ctx.SELECT().accept(this);
}
if (ctx.DISTINCT() != null) {
sb.append(" count(");
ctx.DISTINCT().accept(this);
if (ctx.selectionList().children.size() != 1) {
// FIXME: error message should include query
throw new RuntimeException("Cannot count on more than one column");
if (inSimpleQueryGroup == 1) {
if (ctx.SELECT() != null) {
ctx.SELECT().accept(this);
}
if (ctx.DISTINCT() != null) {
sb.append(" count(");
ctx.DISTINCT().accept(this);
if (ctx.selectionList().children.size() != 1) {
// FIXME: error message should include query
throw new RuntimeException("Cannot count on more than one column");
}
ctx.selectionList().children.get(0).accept(this);
sb.append(" )");
} else {
sb.append(" count( * )");
}
ctx.selectionList().children.get(0).accept(this);
sb.append(" )");
} else {
sb.append(" count( * )");
super.visitSelectClause(ctx);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ public class PanacheJpaUtil {
static final Pattern LONE_SELECT_PATTERN = Pattern.compile(".*SELECT\\s+.*",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

// match a leading WITH
static final Pattern WITH_PATTERN = Pattern.compile("^\\s*WITH\\s+.*",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);

/**
* This turns an HQL (already expanded from Panache-QL) query into a count query, using text manipulation
* if we can, because it's faster, or fall back to using the ORM HQL parser in {@link #getCountQueryUsingParser(String)}
*/
public static String getFastCountQuery(String query) {
// try to generate a good count query from the existing query
String countQuery;
// there are no fast ways to get rid of fetches
if (FETCH_PATTERN.matcher(query).matches()) {
// there are no fast ways to get rid of fetches, or WITH
if (FETCH_PATTERN.matcher(query).matches()
|| WITH_PATTERN.matcher(query).matches()) {
return getCountQueryUsingParser(query);
}
// if it starts with select, we can optimise
Expand Down Expand Up @@ -107,10 +112,13 @@ public static String createFindQuery(Class<?> entityClass, String query, int par
}

String trimmedLc = trimmed.toLowerCase();
if (trimmedLc.startsWith("from ") || trimmedLc.startsWith("select ")) {
if (trimmedLc.startsWith("from ")
|| trimmedLc.startsWith("select ")
|| trimmedLc.startsWith("with ")) {
return query;
}
if (trimmedLc.startsWith("order by ")) {
if (trimmedLc.startsWith("order by ")
|| trimmedLc.startsWith("where ")) {
return "FROM " + getEntityName(entityClass) + " " + query;
}
if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) {
Expand All @@ -135,9 +143,17 @@ public static String createCountQuery(Class<?> entityClass, String query, int pa
return "SELECT COUNT(*) FROM " + getEntityName(entityClass);

String trimmedLc = trimmed.toLowerCase();
// assume these have valid select clauses and let them through
if (trimmedLc.startsWith("select ")
|| trimmedLc.startsWith("with ")) {
return query;
}
if (trimmedLc.startsWith("from ")) {
return "SELECT COUNT(*) " + query;
}
if (trimmedLc.startsWith("where ")) {
return "SELECT COUNT(*) FROM " + getEntityName(entityClass) + " " + query;
}
if (trimmedLc.startsWith("order by ")) {
// ignore it
return "SELECT COUNT(*) FROM " + getEntityName(entityClass);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public void testParser() {
assertCountQueryUsingParser("from bar select count( * )", "from bar select foo");
// from without select
assertCountQueryUsingParser("from bar select count( * )", "from bar");

// CTE
assertFastCountQuery("WITH id AS ( SELECT p.id AS pid FROM Person2 AS p ) SELECT count( * ) FROM Person2 p",
"WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT p FROM Person2 p");
}

@Test
Expand Down Expand Up @@ -58,6 +62,10 @@ public void testFastVersion() {
assertFastCountQuery("from bar select count( * )", "from bar select foo");
// from without select
assertFastCountQuery("SELECT COUNT(*) from bar", "from bar");

// CTE
assertFastCountQuery("WITH id AS ( SELECT p.id AS pid FROM Person2 AS p ) SELECT count( * ) FROM Person2 p",
"WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT p FROM Person2 p");
}

private void assertCountQueryUsingParser(String expected, String selectQuery) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ public String testModel() {
Assertions.assertEquals(1, Person.count("name = :name", Parameters.with("name", "stef").map()));
Assertions.assertEquals(1, Person.count("name = :name", Parameters.with("name", "stef")));
Assertions.assertEquals(1, Person.count("name", "stef"));
Assertions.assertEquals(1, Person.count("from Person2 where name = ?1", "stef"));
Assertions.assertEquals(1, Person.count("where name = ?1", "stef"));
Assertions.assertEquals(1, Person.count("order by name"));

Assertions.assertEquals(1, Dog.count());
Assertions.assertEquals(1, person.dogs.size());
Expand All @@ -110,6 +113,10 @@ public String testModel() {
Assertions.assertEquals(1, persons.size());
Assertions.assertEquals(person, persons.get(0));

persons = Person.find("where name = ?1", "stef").list();
Assertions.assertEquals(1, persons.size());
Assertions.assertEquals(person, persons.get(0));

// full form
persons = Person.find("FROM Person2 WHERE name = ?1", "stef").list();
Assertions.assertEquals(1, persons.size());
Expand Down Expand Up @@ -1824,4 +1831,16 @@ private void testBug26308Query(String hql) {
Assertions.assertEquals(0, query.list().size());
Assertions.assertEquals(0, query.count());
}

@GET
@Path("36496")
@Transactional
public String testBug36496() {
PanacheQuery<Person> query = Person.find("WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT p FROM Person2 p");
Assertions.assertEquals(0, query.list().size());
Assertions.assertEquals(0, query.count());
Assertions.assertEquals(0,
Person.count("WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT count(*) FROM Person2 p"));
return "OK";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,9 @@ void testEnhancement27184DeleteDetached() {
public void testBug26308() {
RestAssured.when().get("/test/26308").then().body(is("OK"));
}

@Test
public void testBug36496() {
RestAssured.when().get("/test/36496").then().body(is("OK"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ public Uni<String> testModel() {
}).flatMap(count -> {
Assertions.assertEquals(1, count);
return Person.count("name", "stef");
}).flatMap(count -> {
Assertions.assertEquals(1, count);
return Person.count("from Person2 where name = ?1", "stef");
}).flatMap(count -> {
Assertions.assertEquals(1, count);
return Person.count("where name = ?1", "stef");
}).flatMap(count -> {
Assertions.assertEquals(1, count);
return Person.count("order by name");
}).flatMap(count -> {
Assertions.assertEquals(1, count);
return Dog.count();
Expand Down Expand Up @@ -104,6 +113,11 @@ public Uni<String> testModel() {
Assertions.assertEquals(1, persons.size());
Assertions.assertEquals(person, persons.get(0));

return Person.find("WHERE name = ?1", "stef").list();
}).flatMap(persons -> {
Assertions.assertEquals(1, persons.size());
Assertions.assertEquals(person, persons.get(0));

// full form
return Person.find("FROM Person2 WHERE name = ?1", "stef").list();
}).flatMap(persons -> {
Expand Down Expand Up @@ -2080,4 +2094,22 @@ private Uni<Void> testBug26308Query(String hql) {
return null;
});
}

@GET
@Path("36496")
@WithTransaction
public Uni<String> testBug36496() {
PanacheQuery<Person> query = Person.find("WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT p FROM Person2 p");
return query.list()
.flatMap(list -> {
Assertions.assertEquals(0, list.size());
return query.count();
}).flatMap(count -> {
Assertions.assertEquals(0, count);
return Person.count("WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT count(*) FROM Person2 p");
}).map(count -> {
Assertions.assertEquals(0, count);
return "OK";
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -309,4 +309,9 @@ public void testBeerRepository() {
public void testBug26308() {
RestAssured.when().get("/test/26308").then().body(is("OK"));
}

@Test
public void testBug36496() {
RestAssured.when().get("/test/36496").then().body(is("OK"));
}
}

0 comments on commit 05b4c23

Please sign in to comment.