Skip to content

Native query pagination fails with derived tables (subqueries in FROM clause)Β #4077

@dongmyo

Description

@dongmyo

Description

When using @Query with nativeQuery = true and Pageable on queries containing derived tables (subqueries in the FROM clause), Spring Data JPA generates incorrect count queries for pagination, resulting in SQL syntax errors.

Versions

  • Spring Boot: 3.5.7
  • Spring Data JPA: 3.5.5
  • Hibernate: 6.6.33.Final
  • Database: MySQL

Current Behavior

The COUNT_MATCH regex pattern in QueryUtils.createCountQueryFor() incorrectly parses native queries with derived tables by matching the FROM keyword inside the subquery instead of the outer query's FROM clause.

Example Query:

select result.order_id, result.latest_activity
from (
    select orders.order_id,
           orders.item_id,
           max(activities.activity_date) as latest_activity
    from order_members
    inner join activities on order_members.order_id = activities.order_id
    where order_members.member_id = ?1
    group by orders.id
) result
where result.latest_activity < ?2

Repository Method:

@Query(value = "...", nativeQuery = true)
Page<Object[]> findOrderActivities(long memberId, Date activityDate, Pageable pageable);

Error:

org.springframework.dao.InvalidDataAccessResourceUsageException:
JDBC exception executing SQL [select result.order_id, result.latest_activity from (
  select orders.order_id from order_members
  group by orders.id]
[You have an error in your SQL syntax; check the manual that
corresponds to your MySQL server version for the right syntax
to use near '' at line 7]

The generated query is truncated, missing:

  • The closing parenthesis of the subquery
  • The outer FROM alias (result)
  • The outer WHERE clause
  • Any ORDER BY clauses

Expected Behavior

The count query should be correctly generated, either by:

  1. Properly parsing the entire query structure, or
  2. Wrapping the entire query in a subquery:
SELECT COUNT(*) FROM (original_query) subquery_alias

Root Cause Analysis

In QueryUtils.java, line 626:

Matcher matcher = COUNT_MATCH.matcher(originalQuery);

The COUNT_MATCH pattern does not account for derived tables and matches the first FROM keyword it encounters, which is inside the subquery. This causes:

  1. Incorrect alias detection (e.g., detecting group as the alias instead of result)
  2. Malformed count query generation
  3. Query string truncation

Steps to Reproduce

  1. Create a repository method with a native query containing a derived table
  2. Use Pageable parameter
  3. Execute the query
  4. Observe SQL syntax error

Minimal Reproducible Example:

@Repository
public interface TestRepository extends JpaRepository<Entity, Long> {

    @Query(value = """
        select u.id, u.name
        from (
            select id, name from users where active = 1
        ) u
        """, nativeQuery = true)
    Page<Object[]> findActiveUsers(Pageable pageable);
}

Additional Context

This issue did not occur in Spring Boot 2.7.x but started appearing after upgrading to Spring Boot 3.x.

The workaround is to manually specify a countQuery:

@Query(value = "...",
       countQuery = "SELECT COUNT(*) FROM (original_query) subquery_alias",
       nativeQuery = true)

However, this is verbose and error-prone when the query changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions