Skip to content

Fix exists() cross-join caused by duplicate CriteriaQuery root#15419

Open
jamesfredley wants to merge 1 commit into7.0.xfrom
fix/exists-cross-join
Open

Fix exists() cross-join caused by duplicate CriteriaQuery root#15419
jamesfredley wants to merge 1 commit into7.0.xfrom
fix/exists-cross-join

Conversation

@jamesfredley
Copy link
Contributor

@jamesfredley jamesfredley commented Feb 20, 2026

Summary

Fixes #14334

GormEntity.exists() was performing a cartesian product (cross-join) against the entire table due to criteriaQuery.from() being called twice in AbstractHibernateGormStaticApi.exists().

Reproducer: https://github.com/scottlollman/grails-data-mapping-issue-2071

Root Cause

AbstractHibernateGormStaticApi.exists() created two query roots:

Root queryRoot = criteriaQuery.from(persistentEntity.javaClass)          // root 1
// ...
criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(...)))     // root 2 (BUG)

Each call to criteriaQuery.from() adds a new root to the query. Two roots on the same table produce a cross-join:

-- Before (cross-join scans entire table)
select count(generatedAlias0)
from SomeDomain as generatedAlias1, SomeDomain as generatedAlias0
where generatedAlias1.id=1L

-- After (single root, simple indexed lookup)
select count(generatedAlias0)
from SomeDomain as generatedAlias0
where generatedAlias0.id=1L

While the boolean result was technically correct (non-zero = true), the query performed a full table scan for every exists() call.

Fix

One-line change - reuse the existing queryRoot variable instead of calling criteriaQuery.from() a second time:

criteriaQuery.select(criteriaBuilder.count(queryRoot))

Tests

Unit Tests (ExistsCrossJoinSpec) - 4 tests

Integration tests using a standalone HibernateDatastore with H2:

  • exists returns true for existing entity
  • exists returns false for non-existent id
  • exists does not produce a cross-join (SQL captured via Hibernate StatementInspector, verified no cross-join or comma-join pattern)
  • exists with multiple rows returns correct result

Functional Tests (ExistsSpec) - 3 tests

Full Grails integration tests in grails-test-examples/gorm using the existing Product domain class:

  • exists returns true for persisted entity
  • exists returns false for non-existent id
  • exists returns correct result with multiple rows in table

Existing ReadOperationSpec tests continue to pass.

@github-actions github-actions bot added the bug label Feb 20, 2026
@jamesfredley jamesfredley self-assigned this Feb 20, 2026
@jamesfredley jamesfredley moved this to In Progress in Apache Grails Feb 20, 2026
@jamesfredley jamesfredley added this to the grails:7.0.8 milestone Feb 20, 2026
AbstractHibernateGormStaticApi.exists() called criteriaQuery.from() twice,
creating a second query root that produced a cartesian product. The generated
SQL selected count(alias0) from Table alias1, Table alias0 where alias1.id=?,
scanning the entire table for every matching row instead of a simple count.

Reuse the existing queryRoot variable for the count select expression.

Fixes #14334

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley marked this pull request as ready for review February 20, 2026 00:28
Copilot AI review requested due to automatic review settings February 20, 2026 00:28
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Hibernate CriteriaQuery inefficiency in GormEntity.exists() where calling criteriaQuery.from() twice created duplicate roots and caused a cartesian product (cross-join), leading to full table scans.

Changes:

  • Reuse the existing CriteriaQuery root in AbstractHibernateGormStaticApi.exists() to avoid introducing a second root (and cross-join).
  • Add a core test (ExistsCrossJoinSpec) that captures SQL via StatementInspector to assert no cross-join/comma-join is generated for exists().
  • Add an integration test example (ExistsSpec) in grails-test-examples/gorm validating exists() behavior in a full Grails app context.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
grails-data-hibernate5/core/src/main/groovy/org/grails/orm/hibernate/AbstractHibernateGormStaticApi.groovy Fixes the duplicate-root CriteriaQuery construction by counting the existing queryRoot.
grails-data-hibernate5/core/src/test/groovy/org/grails/orm/hibernate/ExistsCrossJoinSpec.groovy Adds regression coverage to ensure exists() does not generate cross-joins in SQL.
grails-test-examples/gorm/src/integration-test/groovy/gorm/ExistsSpec.groovy Adds functional/integration validation of exists() in the sample app.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming the test failure is due to test flakyness

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

Domain exists method cross joining entire table with Hibernate

2 participants

Comments