quarkusio/quarkus · error · PanacheQueryException

Unable to perform a projection on a 'select [distinct]? new'

Error message

Unable to perform a projection on a 'select [distinct]? new' query: ${query}

What it means

Panache's project(Class) maps result rows onto a projection type. Queries using constructor expressions — 'select new com.Foo(...)' or 'select distinct new ...' — already instantiate objects themselves, so applying another projection is ambiguous and Panache rejects it with PanacheQueryException.

Source

Thrown at extensions/panache/hibernate-orm-panache-common/runtime/src/main/java/io/quarkus/hibernate/orm/panache/common/runtime/CommonPanacheQueryImpl.java:138

    // Builder

    public CommonPanacheQueryImpl<Entity> sort(Sort sort) {
        this.sort = sort;
        return this;
    }

    public <T> CommonPanacheQueryImpl<T> project(Class<T> type) {
        String selectQuery = query;
        if (PanacheJpaUtil.isNamedQuery(query)) {
            SelectionQuery<?> q = session.createNamedSelectionQuery(query.substring(1));
            selectQuery = getQueryString(q);
        }

        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 pass it on to ORM which can handle that via a projection type
        if (lowerCasedTrimmedQuery.startsWith("select ")) {
            // I think projections do not change the result count, so we can keep the custom count query
            return new CommonPanacheQueryImpl<>(this, query, customCountQueryForSpring, type);
        }

        // FIXME: this assumes the query starts with "FROM " probably?

        // build select clause with a constructor expression
        AtomicReference<String> cachedProjection = ProjectionQueryCache.get(type);
        if (cachedProjection.get() == null) {
            cachedProjection.set("SELECT " + getParametersFromClass(type, null));
        }
        String selectClause = cachedProjection.get();
        // I think projections do not change the result count, so we can keep the custom count query
        return new CommonPanacheQueryImpl<>(this, selectClause + selectQuery, customCountQueryForSpring, null);

View on GitHub (pinned to e1c734241f)

Solutions

  1. Remove .project(...) and let the 'select new' constructor expression do the mapping
  2. Strip the 'select new ...' clause and select plain entity fields, then use project(Dto.class)
  3. Use a literal 'select field1, field2' query with project() instead

Example fix

// before
PanacheQuery<Person> q = Person.find("select new com.acme.PersonView(p.name) from Person p").project(PersonView.class);
// after
PanacheQuery<Person> q = Person.find("select p.name from Person p").project(PersonView.class);
Defensive patterns

Strategy: validation

Validate before calling

String lc = query.toLowerCase().trim();
if (lc.startsWith("select new ") || lc.startsWith("select distinct new ")) {
    throw new IllegalArgumentException("Remove .project(); query already uses a constructor expression");
}

Try / catch

try {
    return find(query, params).project(dtoClass);
} catch (PanacheQueryException e) {
    if (e.getMessage() != null && e.getMessage().contains("Unable to perform a projection")) {
        return find(query, params); // constructor expression already maps results
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling .project(SomeDto.class) on a query string that begins with 'select new ' or 'select distinct new '.

Common situations: Refactoring a DTO-constructor query to the project() API; Spring-Data-style projections copied over that already contain 'select new'; generated queries from builders that always emit a select clause.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/f35b278c48ff3df7. Report an issue: GitHub.