Skip to content

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

Merged
jdaugherty merged 1 commit into7.0.xfrom
fix/exists-cross-join
Feb 20, 2026
Merged

Fix exists() cross-join caused by duplicate CriteriaQuery root#15419
jdaugherty merged 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

@jdaugherty jdaugherty merged commit d430eed into 7.0.x Feb 20, 2026
59 of 60 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in Apache Grails Feb 20, 2026
@jdaugherty jdaugherty deleted the fix/exists-cross-join branch February 20, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Domain exists method cross joining entire table with Hibernate

3 participants