From 7343746420382813d71d2b65f5b852d4ae0a9331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Wed, 21 Feb 2024 16:18:19 +0100 Subject: [PATCH] Delegate projections with a `select` clause to ORM Problem is that there's a bug in ORM that disallows projections of single columns, but that will be resolved by https://github.com/hibernate/hibernate-orm/pull/7874 Fixes #31117 --- .../runtime/CommonPanacheQueryImpl.java | 38 +++---- .../runtime/CommonPanacheQueryImpl.java | 61 +++++------- .../common/runtime/PanacheJpaUtil.java | 98 ++++++++++++------- .../io/quarkus/it/panache/TestEndpoint.java | 19 +++- .../it/panache/PanacheFunctionalityTest.java | 5 + .../it/panache/reactive/TestEndpoint.java | 3 +- 6 files changed, 123 insertions(+), 101 deletions(-) diff --git a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java index f9dd38ee25652b..bd1b523a53d574 100644 --- a/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java @@ -59,6 +59,7 @@ public void close() { private Map hints; private Map> filters; + private Class projectionType; public CommonPanacheQueryImpl(EntityManager em, String query, String originalQuery, String orderBy, Object paramsArrayOrMap) { @@ -69,7 +70,8 @@ public CommonPanacheQueryImpl(EntityManager em, String query, String originalQue this.paramsArrayOrMap = paramsArrayOrMap; } - private CommonPanacheQueryImpl(CommonPanacheQueryImpl previousQuery, String newQueryString, String countQuery) { + private CommonPanacheQueryImpl(CommonPanacheQueryImpl previousQuery, String newQueryString, String countQuery, + Class projectionType) { this.em = previousQuery.em; this.query = newQueryString; this.countQuery = countQuery; @@ -81,6 +83,7 @@ private CommonPanacheQueryImpl(CommonPanacheQueryImpl previousQuery, String n this.lockModeType = previousQuery.lockModeType; this.hints = previousQuery.hints; this.filters = previousQuery.filters; + this.projectionType = projectionType; } // Builder @@ -92,39 +95,24 @@ public CommonPanacheQueryImpl project(Class type) { selectQuery = q.getQueryString(); } - String lowerCasedTrimmedQuery = selectQuery.trim().replace('\n', ' ').replace('\r', ' ').toLowerCase(); + String lowerCasedTrimmedQuery = PanacheJpaUtil.trimForAnalysis(selectQuery); if (lowerCasedTrimmedQuery.startsWith("select new ") || lowerCasedTrimmedQuery.startsWith("select distinct new ")) { throw new PanacheQueryException("Unable to perform a projection on a 'select [distinct]? new' query: " + query); } - // If the query starts with a select clause, we generate an HQL query - // using the fields in the select clause: - // Initial query: select e.field1, e.field2 from EntityClass e - // New query: SELECT new org.acme.ProjectionClass(e.field1, e.field2) from EntityClass e + // If the query starts with a select clause, we pass it on to ORM which can handle that via a projection type if (lowerCasedTrimmedQuery.startsWith("select ")) { - int endSelect = lowerCasedTrimmedQuery.indexOf(" from "); - String trimmedQuery = selectQuery.trim().replace('\n', ' ').replace('\r', ' '); - // 7 is the length of "select " - String selectClause = trimmedQuery.substring(7, endSelect).trim(); - String from = trimmedQuery.substring(endSelect); - StringBuilder newQuery = new StringBuilder("select "); - // Handle select-distinct. HQL example: select distinct new org.acme.ProjectionClass... - boolean distinctQuery = selectClause.toLowerCase().startsWith("distinct "); - if (distinctQuery) { - // 9 is the length of "distinct " - selectClause = selectClause.substring(9).trim(); - newQuery.append("distinct "); - } - - newQuery.append("new ").append(type.getName()).append("(").append(selectClause).append(")").append(from); - return new CommonPanacheQueryImpl<>(this, newQuery.toString(), "select count(*) " + from); + // just pass it through + return new CommonPanacheQueryImpl<>(this, query, countQuery, type); } + // FIXME: this assumes the query starts with "FROM " probably? + // build select clause with a constructor expression String selectClause = "SELECT " + getParametersFromClass(type, null); return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery, - "select count(*) " + selectQuery); + "select count(*) " + selectQuery, null); } private StringBuilder getParametersFromClass(Class type, String parentParameter) { @@ -392,10 +380,10 @@ private Query createBaseQuery() { Query jpaQuery; if (PanacheJpaUtil.isNamedQuery(query)) { String namedQuery = query.substring(1); - jpaQuery = em.createNamedQuery(namedQuery); + jpaQuery = em.createNamedQuery(namedQuery, projectionType); } else { try { - jpaQuery = em.createQuery(orderBy != null ? query + orderBy : query); + jpaQuery = em.createQuery(orderBy != null ? query + orderBy : query, projectionType); } catch (IllegalArgumentException x) { throw NamedQueryUtil.checkForNamedQueryMistake(x, originalQuery); } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java index 4173dac1973d73..5fb76478259daa 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/CommonPanacheQueryImpl.java @@ -51,6 +51,7 @@ public class CommonPanacheQueryImpl { private Map hints; private Map> filters; + private Class projectionType; public CommonPanacheQueryImpl(Uni em, String query, String originalQuery, String orderBy, Object paramsArrayOrMap) { @@ -61,7 +62,8 @@ public CommonPanacheQueryImpl(Uni em, String query, String origi this.paramsArrayOrMap = paramsArrayOrMap; } - private CommonPanacheQueryImpl(CommonPanacheQueryImpl previousQuery, String newQueryString, String countQuery) { + private CommonPanacheQueryImpl(CommonPanacheQueryImpl previousQuery, String newQueryString, String countQuery, + Class projectionType) { this.em = previousQuery.em; this.query = newQueryString; this.countQuery = countQuery; @@ -73,6 +75,7 @@ private CommonPanacheQueryImpl(CommonPanacheQueryImpl previousQuery, String n this.lockModeType = previousQuery.lockModeType; this.hints = previousQuery.hints; this.filters = previousQuery.filters; + this.projectionType = projectionType; } // Builder @@ -83,38 +86,24 @@ public CommonPanacheQueryImpl project(Class type) { selectQuery = NamedQueryUtil.getNamedQuery(query.substring(1)); } - String lowerCasedTrimmedQuery = selectQuery.trim().toLowerCase(); - if (lowerCasedTrimmedQuery.startsWith("select new ")) { - throw new PanacheQueryException("Unable to perform a projection on a 'select new' query: " + query); + String lowerCasedTrimmedQuery = PanacheJpaUtil.trimForAnalysis(selectQuery); + if (lowerCasedTrimmedQuery.startsWith("select new ") + || lowerCasedTrimmedQuery.startsWith("select distinct new ")) { + throw new PanacheQueryException("Unable to perform a projection on a 'select [distinct]? new' query: " + query); } - // If the query starts with a select clause, we generate an HQL query - // using the fields in the select clause: - // Initial query: select e.field1, e.field2 from EntityClass e - // New query: SELECT new org.acme.ProjectionClass(e.field1, e.field2) from EntityClass e + // If the query starts with a select clause, we pass it on to ORM which can handle that via a projection type if (lowerCasedTrimmedQuery.startsWith("select ")) { - int endSelect = lowerCasedTrimmedQuery.indexOf(" from "); - String trimmedQuery = selectQuery.trim(); - // 7 is the length of "select " - String selectClause = trimmedQuery.substring(7, endSelect).trim(); - String from = trimmedQuery.substring(endSelect); - StringBuilder newQuery = new StringBuilder("select "); - // Handle select-distinct. HQL example: select distinct new org.acme.ProjectionClass... - boolean distinctQuery = selectClause.toLowerCase().startsWith("distinct "); - if (distinctQuery) { - // 9 is the length of "distinct " - selectClause = selectClause.substring(9).trim(); - newQuery.append("distinct "); - } - - newQuery.append("new ").append(type.getName()).append("(").append(selectClause).append(")").append(from); - return new CommonPanacheQueryImpl<>(this, newQuery.toString(), "select count(*) " + from); + // just pass it through + return new CommonPanacheQueryImpl<>(this, query, countQuery, type); } + // FIXME: this assumes the query starts with "FROM " probably? + // build select clause with a constructor expression String selectClause = "SELECT " + getParametersFromClass(type, null); return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery, - "select count(*) " + selectQuery); + "select count(*) " + selectQuery, type); } private StringBuilder getParametersFromClass(Class type, String parentParameter) { @@ -305,7 +294,7 @@ private String countQuery(String selectQuery) { @SuppressWarnings({ "unchecked", "rawtypes" }) public Uni> list() { return em.flatMap(session -> { - Mutiny.Query jpaQuery = createQuery(session); + Mutiny.SelectionQuery jpaQuery = createQuery(session); return (Uni) applyFilters(session, () -> jpaQuery.getResultList()); }); } @@ -323,7 +312,7 @@ public Multi stream() { @SuppressWarnings("unchecked") public Uni firstResult() { return em.flatMap(session -> { - Mutiny.Query jpaQuery = createQuery(session, 1); + Mutiny.SelectionQuery jpaQuery = createQuery(session, 1); return applyFilters(session, () -> jpaQuery.getResultList().map(list -> list.isEmpty() ? null : (T) list.get(0))); }); } @@ -331,15 +320,15 @@ public Uni firstResult() { @SuppressWarnings("unchecked") public Uni singleResult() { return em.flatMap(session -> { - Mutiny.Query jpaQuery = createQuery(session); + Mutiny.SelectionQuery jpaQuery = createQuery(session); return applyFilters(session, () -> jpaQuery.getSingleResult().map(v -> (T) v)) // FIXME: workaround https://github.com/hibernate/hibernate-reactive/issues/263 .onFailure(CompletionException.class).transform(t -> t.getCause()); }); } - private Mutiny.Query createQuery(Mutiny.Session em) { - Mutiny.Query jpaQuery = createBaseQuery(em); + private Mutiny.SelectionQuery createQuery(Mutiny.Session em) { + Mutiny.SelectionQuery jpaQuery = createBaseQuery(em); if (range != null) { jpaQuery.setFirstResult(range.getStartIndex()); @@ -364,8 +353,8 @@ private Mutiny.Query createQuery(Mutiny.Session em) { return jpaQuery; } - private Mutiny.Query createQuery(Mutiny.Session em, int maxResults) { - Mutiny.Query jpaQuery = createBaseQuery(em); + private Mutiny.SelectionQuery createQuery(Mutiny.Session em, int maxResults) { + Mutiny.SelectionQuery jpaQuery = createBaseQuery(em); if (range != null) { jpaQuery.setFirstResult(range.getStartIndex()); @@ -385,14 +374,14 @@ private Mutiny.Query createQuery(Mutiny.Session em, int maxResults) { } @SuppressWarnings("unchecked") - private Mutiny.Query createBaseQuery(Mutiny.Session em) { - Mutiny.Query jpaQuery; + private Mutiny.SelectionQuery createBaseQuery(Mutiny.Session em) { + Mutiny.SelectionQuery jpaQuery; if (PanacheJpaUtil.isNamedQuery(query)) { String namedQuery = query.substring(1); - jpaQuery = em.createNamedQuery(namedQuery); + jpaQuery = em.createNamedQuery(namedQuery, projectionType); } else { try { - jpaQuery = em.createQuery(orderBy != null ? query + orderBy : query); + jpaQuery = em.createQuery(orderBy != null ? query + orderBy : query, projectionType); } catch (IllegalArgumentException x) { throw NamedQueryUtil.checkForNamedQueryMistake(x, originalQuery); } diff --git a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java index 13230c3c28c2de..445932b735f17d 100644 --- a/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java +++ b/extensions/panache/panache-hibernate-common/runtime/src/main/java/io/quarkus/panache/hibernate/common/runtime/PanacheJpaUtil.java @@ -1,5 +1,6 @@ package io.quarkus.panache.hibernate.common.runtime; +import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -51,8 +52,13 @@ public static String getFastCountQuery(String query) { Matcher selectMatcher = SELECT_PATTERN.matcher(query); if (selectMatcher.matches()) { // this one cannot be null - String firstSelection = selectMatcher.group(1).trim(); - if (firstSelection.toLowerCase().startsWith("distinct ")) { + String firstSelection = selectMatcher.group(1).trim().toLowerCase(Locale.ROOT); + if (firstSelection.startsWith("distinct")) { + // if firstSelection matched distinct only, we have something wrong in our selection list, probably functions/parens + // so bail out + if (firstSelection.length() == 8) { + return getCountQueryUsingParser(query); + } // this one can be null String secondSelection = selectMatcher.group(2); // we can only count distinct single columns @@ -101,27 +107,44 @@ public static String getEntityName(Class entityClass) { return entityClass.getName(); } + /** + * Removes \n, \r and outside spaces, and turns to lower case. DO NOT USE the result to pass it on to ORM, + * because the query is likely to be invalid since we replace newlines even if they + * are in quoted strings. This is only useful to analyse the start of the query for + * quick processing. NEVER use this to pass it to the DB or to replace user queries. + */ + public static String trimForAnalysis(String query) { + // first replace single chars \n\r\t to spaces + // turn to lower case + String ret = query.replace('\n', ' ').replace('\r', ' ').replace('\t', ' ').toLowerCase(Locale.ROOT); + // if we have more than one space, replace with one + if (ret.indexOf(" ") != -1) { + ret = ret.replaceAll("\\s+", " "); + } + // replace outer spaces + return ret.trim(); + } + public static String createFindQuery(Class entityClass, String query, int paramCount) { if (query == null) { return "FROM " + getEntityName(entityClass); } - String trimmed = query.replace('\n', ' ').replace('\r', ' ').trim(); - if (trimmed.isEmpty()) { + String trimmedForAnalysis = trimForAnalysis(query); + if (trimmedForAnalysis.isEmpty()) { return "FROM " + getEntityName(entityClass); } - String trimmedLc = trimmed.toLowerCase(); - if (trimmedLc.startsWith("from ") - || trimmedLc.startsWith("select ") - || trimmedLc.startsWith("with ")) { + if (trimmedForAnalysis.startsWith("from ") + || trimmedForAnalysis.startsWith("select ") + || trimmedForAnalysis.startsWith("with ")) { return query; } - if (trimmedLc.startsWith("order by ") - || trimmedLc.startsWith("where ")) { + if (trimmedForAnalysis.startsWith("order by ") + || trimmedForAnalysis.startsWith("where ")) { return "FROM " + getEntityName(entityClass) + " " + query; } - if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) { + if (trimmedForAnalysis.indexOf(' ') == -1 && trimmedForAnalysis.indexOf('=') == -1 && paramCount == 1) { query += " = ?1"; } return "FROM " + getEntityName(entityClass) + " WHERE " + query; @@ -138,27 +161,26 @@ public static String createCountQuery(Class entityClass, String query, int pa if (query == null) return "SELECT COUNT(*) FROM " + getEntityName(entityClass); - String trimmed = query.trim(); - if (trimmed.isEmpty()) + String trimmedForAnalysis = trimForAnalysis(query); + if (trimmedForAnalysis.isEmpty()) 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 ")) { + if (trimmedForAnalysis.startsWith("select ") + || trimmedForAnalysis.startsWith("with ")) { return query; } - if (trimmedLc.startsWith("from ")) { + if (trimmedForAnalysis.startsWith("from ")) { return "SELECT COUNT(*) " + query; } - if (trimmedLc.startsWith("where ")) { + if (trimmedForAnalysis.startsWith("where ")) { return "SELECT COUNT(*) FROM " + getEntityName(entityClass) + " " + query; } - if (trimmedLc.startsWith("order by ")) { + if (trimmedForAnalysis.startsWith("order by ")) { // ignore it return "SELECT COUNT(*) FROM " + getEntityName(entityClass); } - if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) { + if (trimmedForAnalysis.indexOf(' ') == -1 && trimmedForAnalysis.indexOf('=') == -1 && paramCount == 1) { query += " = ?1"; } return "SELECT COUNT(*) FROM " + getEntityName(entityClass) + " WHERE " + query; @@ -169,26 +191,29 @@ public static String createUpdateQuery(Class entityClass, String query, int p throw new PanacheQueryException("Query string cannot be null"); } - String trimmed = query.trim(); - if (trimmed.isEmpty()) { + String trimmedForAnalysis = trimForAnalysis(query); + if (trimmedForAnalysis.isEmpty()) { throw new PanacheQueryException("Query string cannot be empty"); } - String trimmedLc = trimmed.toLowerCase(); // backwards compat trying to be helpful, remove the from - if (trimmedLc.startsWith("update from")) { - return "update " + trimmed.substring(11); + if (trimmedForAnalysis.startsWith("update from")) { + // find the original from and skip it + int index = query.toLowerCase(Locale.ROOT).indexOf("from"); + return "update " + query.substring(index + 4); } - if (trimmedLc.startsWith("update ")) { + if (trimmedForAnalysis.startsWith("update ")) { return query; } - if (trimmedLc.startsWith("from ")) { - return "UPDATE " + trimmed.substring(5); + if (trimmedForAnalysis.startsWith("from ")) { + // find the original from and skip it + int index = query.toLowerCase(Locale.ROOT).indexOf("from"); + return "UPDATE " + query.substring(index + 4); } - if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) { + if (trimmedForAnalysis.indexOf(' ') == -1 && trimmedForAnalysis.indexOf('=') == -1 && paramCount == 1) { query += " = ?1"; } - if (trimmedLc.startsWith("set ")) { + if (trimmedForAnalysis.startsWith("set ")) { return "UPDATE " + getEntityName(entityClass) + " " + query; } return "UPDATE " + getEntityName(entityClass) + " SET " + query; @@ -198,22 +223,21 @@ public static String createDeleteQuery(Class entityClass, String query, int p if (query == null) return "DELETE FROM " + getEntityName(entityClass); - String trimmed = query.trim(); - if (trimmed.isEmpty()) + String trimmedForAnalysis = trimForAnalysis(query); + if (trimmedForAnalysis.isEmpty()) return "DELETE FROM " + getEntityName(entityClass); - String trimmedLc = trimmed.toLowerCase(); - if (trimmedLc.startsWith("delete ")) { + if (trimmedForAnalysis.startsWith("delete ")) { return query; } - if (trimmedLc.startsWith("from ")) { + if (trimmedForAnalysis.startsWith("from ")) { return "DELETE " + query; } - if (trimmedLc.startsWith("order by ")) { + if (trimmedForAnalysis.startsWith("order by ")) { // ignore it return "DELETE FROM " + getEntityName(entityClass); } - if (trimmedLc.indexOf(' ') == -1 && trimmedLc.indexOf('=') == -1 && paramCount == 1) { + if (trimmedForAnalysis.indexOf(' ') == -1 && trimmedForAnalysis.indexOf('=') == -1 && paramCount == 1) { query += " = ?1"; } return "DELETE FROM " + getEntityName(entityClass) + " WHERE " + query; diff --git a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java index b2e8f6a9657af0..70c504668e2713 100644 --- a/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java +++ b/integration-tests/hibernate-orm-panache/src/main/java/io/quarkus/it/panache/TestEndpoint.java @@ -1341,8 +1341,7 @@ public String testProjection() { Assertions.assertNull(aggregationDistinctProjection.getOwnerName()); Assertions.assertEquals(bubulle.weight, aggregationDistinctProjection.getWeight()); - long countDistinct = projectionDistinctQuery.count(); - Assertions.assertEquals(1L, countDistinct); + Assertions.assertThrows(RuntimeException.class, () -> projectionDistinctQuery.count()); // We are checking that not everything gets lowercased PanacheQuery letterCaseQuery = Cat @@ -1843,4 +1842,20 @@ public String testBug36496() { Person.count("WITH id AS (SELECT p.id AS pid FROM Person2 AS p) SELECT count(*) FROM Person2 p")); return "OK"; } + + @GET + @Path("31117") + @Transactional + public String testBug31117() { + Person.deleteAll(); + Person p = new Person(); + p.name = "stef"; + p.persist(); + Assertions.assertEquals(1, Person.find("\r\n \n\nfrom\n Person2\nwhere\n\rname = ?1", "stef").list().size()); + Assertions.assertEquals(1, Person.find("\r\n \n\nfrom\n Person2\nwhere\n\rname = ?1", "stef").count()); + Assertions.assertEquals(1, Person.count("\r\n \n\nfrom\n Person2\nwhere\n\rname = ?1", "stef")); + Assertions.assertEquals(1, Person.update("\r\n \n\nupdate\n Person2\nset\n\rname='foo' where\n\rname = ?1", "stef")); + Assertions.assertEquals(1, Person.delete("\r\n \n\ndelete\nfrom\n Person2\nwhere\nname = ?1", "foo")); + return "OK"; + } } diff --git a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java index 6ae13eb3efa5af..5d7883e050d609 100644 --- a/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java +++ b/integration-tests/hibernate-orm-panache/src/test/java/io/quarkus/it/panache/PanacheFunctionalityTest.java @@ -254,4 +254,9 @@ public void testBug26308() { public void testBug36496() { RestAssured.when().get("/test/36496").then().body(is("OK")); } + + @Test + public void testBug31117() { + RestAssured.when().get("/test/31117").then().body(is("OK")); + } } diff --git a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java index 7a149d0645a79f..9b655b59b1c679 100644 --- a/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java +++ b/integration-tests/hibernate-reactive-panache/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java @@ -1715,7 +1715,8 @@ public Uni testProjection2() { () -> Cat.find("select new FakeClass('fake_cat', 'fake_owner', 12.5) from Cat c") .project(CatProjectionBean.class)); Assertions.assertTrue( - exception.getMessage().startsWith("Unable to perform a projection on a 'select new' query")); + exception.getMessage() + .startsWith("Unable to perform a projection on a 'select [distinct]? new' query")); }) .chain(() -> Cat .find(" SELECT disTINct 'GARFIELD', 'JoN ArBuCkLe' from Cat c where name = :NamE group by name ",