Skip to content

Clean up AbstractCriteriaBuilder: remove dead code, add test coverage, fix warnings - #16140

Open
borinquenkid wants to merge 5 commits into
8.1.xfrom
chore/cleanup-AbstractCriteriaBuilder
Open

Clean up AbstractCriteriaBuilder: remove dead code, add test coverage, fix warnings#16140
borinquenkid wants to merge 5 commits into
8.1.xfrom
chore/cleanup-AbstractCriteriaBuilder

Conversation

@borinquenkid

Copy link
Copy Markdown
Member

Summary

Stacked on #16135 (needs that merged first). Cleans up AbstractCriteriaBuilder and its two concrete subclasses (grails.gorm.CriteriaBuilder, grails.gorm.rx.CriteriaBuilder), which had zero direct test coverage and hadn't been reviewed for a while.

  • Dead code removal: grails.gorm.CriteriaBuilder's cache/readOnly/join(String)/select overrides were byte-for-byte duplicates of AbstractCriteriaBuilder's own bodies, existing only to narrow the return type from Criteria to BuildableCriteria for fluent chaining. Replaced each with a cast-and-delegate to super, matching the pattern already used by grails.gorm.rx.DetachedCriteria.
  • Real bug fix: grails.gorm.rx.CriteriaBuilder.count(Map, Closure) assigned the void return of prepareQuery(...) to a local query variable, which Groovy evaluates as null, shadowing the real query field for the rest of the method — a guaranteed NPE on every real call, never caught because the module had zero tests.
  • Test coverage added: new Specs for both concrete subclasses (mocked Query/QueryCreator/MappingContext collaborators, since the class only builds/delegates Query.Criterion objects rather than persisting anything). AbstractCriteriaBuilder 0% → 99.7% lines / 100% methods / 90% branches; both CriteriaBuilders → 100% lines / 100% methods.
  • PMD + IntelliJ inspection cleanup on AbstractCriteriaBuilder: added missing @Override annotations, removed a dead initializer, reordered string comparisons to put the known constant first (avoids NPE if compared value is null), parameterized raw generic types, converted instanceof+cast to pattern variables, replaced a redundant instanceof AssociationQuery check with a null check (confirmed Query.createQuery(String)'s declared return type is AssociationQuery itself), replaced manual list.get(size()-1)/.remove(size()-1) with getLast()/removeLast(), and extracted a duplicated MetaMethod lookup block into a helper (using a sentinel to preserve exact null-vs-not-found semantics). Two items left as accepted/suppressed with comments explaining why: in/inList(String, Collection) keep a raw Collection parameter to match Criteria's own raw interface declaration (can't be fixed without touching the shared interface), and addToCriteria keeps its non-void return since a few callers legitimately use it.

Test plan

  • :grails-datamapping-core:test — all passing
  • :grails-datamapping-rx:test — all passing
  • :grails-datamapping-core:codeStyle / :grails-datamapping-rx:codeStyle — clean
  • Coverage verified via jacocoTestReport at each step

🤖 Generated with Claude Code

borinquenkid and others added 2 commits August 12, 2026 08:14
AbstractCriteriaBuilder had zero direct unit tests and, since it's abstract,
can only be exercised through its concrete subclasses. Adds CriteriaBuilderSpec
in grails-datamapping-core (via grails.gorm.CriteriaBuilder) and a matching
spec in grails-datamapping-rx (via grails.gorm.rx.CriteriaBuilder), using
mocked Query/QueryCreator/MappingContext collaborators since the class only
builds and delegates Query.Criterion objects rather than persisting anything
itself. Coverage: AbstractCriteriaBuilder 0% -> 99.7% lines / 100% methods;
CriteriaBuilder (both sync and rx) -> 100% lines / 100% methods.

Along the way:
- grails.gorm.CriteriaBuilder's cache/readOnly/join(String)/select overrides
  were byte-for-byte duplicates of AbstractCriteriaBuilder's own bodies,
  existing only to narrow the return type from Criteria to BuildableCriteria
  for fluent chaining. Replaced each with a cast-and-delegate to super,
  matching the pattern already used by grails.gorm.rx.DetachedCriteria.
- Fixed a real, previously-undetected bug in grails.gorm.rx.CriteriaBuilder.count(Map, Closure):
  it assigned the void return of prepareQuery(...) to a local `query` variable,
  which Groovy evaluates as null, shadowing the real query field for the rest
  of the method. Guaranteed NPE on every real call; never caught because the
  module had zero test coverage before being re-enabled.
- Cleaned up AbstractCriteriaBuilder per PMD (run as a local, temporary
  diagnostic only - not applied to the build): added 47 missing @OverRide
  annotations, removed a dead initializer, and reordered 4 string comparisons
  to put the known constant first (avoids NPE if the compared value is null).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Removed 2 truly-unused public constants (ORDER_DESCENDING/ORDER_ASCENDING)
  after confirming zero references anywhere in the repo; the class's own
  order(String, String) already used grails.gorm.CriteriaBuilder's identical
  constants instead.
- Parameterized raw uses of Class, PersistentProperty, Association, Closure,
  QueryableCriteria, and DetachedCriteria.
- Converted 3 instanceof-plus-cast blocks to Java pattern variables.
- Replaced a redundant `associationQuery instanceof AssociationQuery` check
  with a null check, since Query.createQuery(String) is declared to return
  AssociationQuery directly, making the instanceof always true.
- Replaced manual list.get(size()-1)/remove(size()-1) with getLast()/removeLast().
- Extracted the duplicated MetaMethod lookup-and-invoke block in invokeMethod
  into a private helper, using a NOT_FOUND sentinel to preserve the exact
  "found but returned null" vs "not found" distinction the original two
  copies each handled inline.
- Left 2 items as accepted/suppressed rather than "fixed" incorrectly:
  in(String, Collection) and inList(String, Collection) keep a raw Collection
  parameter with @SuppressWarnings("rawtypes"), since Criteria's own
  declaration is raw and a parameterized override would not actually
  override it (name clash, not an override, due to erasure); addToCriteria
  keeps @SuppressWarnings("UnusedReturnValue") since most callers discard its
  return value by design, even though a few legitimately use it.
- Also drops an unused Criteria import from grails.gorm.rx.CriteriaBuilder.

AbstractCriteriaBuilder coverage held at 99.7% lines / 100% methods / 90%
branches throughout; all changes verified semantic-preserving.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 12, 2026 18:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request refactors and hardens the GORM criteria builder implementation by cleaning up AbstractCriteriaBuilder, fixing a concrete RxGORM bug, and adding direct unit test coverage for the criteria builder DSL and reactive terminal operations.

Changes:

  • Fixes grails.gorm.rx.CriteriaBuilder.count(Map, Closure) by removing an accidental shadowing assignment that would always yield null and NPE at runtime.
  • Refactors org.grails.datastore.gorm.query.criteria.AbstractCriteriaBuilder to remove dead/duplicated logic and address static analysis warnings (generics, @Override, safer string comparisons, pattern variables, small helper extraction).
  • Adds new Spock specs to directly cover both grails.gorm.CriteriaBuilder and grails.gorm.rx.CriteriaBuilder behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
grails-datamapping-rx/src/main/groovy/grails/gorm/rx/CriteriaBuilder.groovy Fixes count(Map, Closure) logic so the prepared query isn’t replaced with null.
grails-datamapping-rx/src/test/groovy/grails/gorm/rx/CriteriaBuilderSpec.groovy Adds reactive terminal-operation coverage for Rx criteria builder.
grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/criteria/AbstractCriteriaBuilder.java Refactors core criteria DSL dispatch and validation; addresses warnings and improves maintainability.
grails-datamapping-core/src/main/groovy/grails/gorm/CriteriaBuilder.java Removes duplicate override bodies by delegating to super and casting for fluent chaining.
grails-datamapping-core/src/test/groovy/grails/gorm/CriteriaBuilderSpec.groovy Adds extensive coverage for criteria DSL and builder behaviors via mocked datastore/query collaborators.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread grails-datamapping-rx/src/test/groovy/grails/gorm/rx/CriteriaBuilderSpec.groovy Outdated
Comment on lines +62 to +66
CriteriaBuilder<CriteriaBuilderTestPerson> newBuilder() {
def criteria = new CriteriaBuilder<CriteriaBuilderTestPerson>(CriteriaBuilderTestPerson, queryCreator, mappingContext)
criteria.@query = query
criteria
}
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.9580%. Comparing base (2632f6f) to head (db5a42e).

Files with missing lines Patch % Lines
...e/gorm/query/criteria/AbstractCriteriaBuilder.java 91.1765% 1 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                         Coverage Diff                          @@
##             feat/enable-datamapping-rx     #16140        +/-   ##
====================================================================
+ Coverage                       52.8175%   52.9580%   +0.1405%     
- Complexity                        18864      18947        +83     
====================================================================
  Files                              2079       2079                
  Lines                             97197      97194         -3     
  Branches                          16870      16871         +1     
====================================================================
+ Hits                              51337      51472       +135     
+ Misses                            38446      38333       -113     
+ Partials                           7414       7389        -25     
Files with missing lines Coverage Δ
...e/src/main/groovy/grails/gorm/CriteriaBuilder.java 100.0000% <100.0000%> (+26.6667%) ⬆️
.../main/groovy/grails/gorm/rx/CriteriaBuilder.groovy 96.6667% <ø> (+3.3333%) ⬆️
...e/gorm/query/criteria/AbstractCriteriaBuilder.java 98.7730% <91.1765%> (+35.3884%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@borinquenkid borinquenkid added this to the grails:8.1.0-M1 milestone Aug 12, 2026
borinquenkid and others added 3 commits August 12, 2026 16:39
Copilot review: both CriteriaBuilderSpec.groovy files set the builder's internal `query`
field directly (`criteria.@query = query`) to seed a mocked query, bypassing the public
construction/init path. Removed it from the core spec's newBuilder() and three
individually-constructed tests, since queryCreator.createQuery(...) was already stubbed to
return the same mock and ensureQueryIsInitialized() wires it up lazily through the real
public path. For cases that need the query pre-set before any DSL call runs (the projection
accessor tests, which don't call ensureQueryIsInitialized() themselves), switched
newBuilder() to the public CriteriaBuilder(Class, Session, Query) constructor instead.
Removed the same field write in the rx spec, where every test already goes through a real
entry point that initializes the query itself.

Codecov: patch coverage on AbstractCriteriaBuilder.java was 84% with 1 missing + 3 partial
lines. Added targeted tests for: an association subquery whose query creator returns null,
a nested association closure whose associated entity is unresolvable (exercises the
persistentEntity == null early return in validatePropertyName), the SCROLL_CALL construction
check's non-matching argument-count/type branches, invokeClosureNode with a non-closure
argument (call with a non-closure arg), handleJunction with a null callable (and(null)), and
ensureQueryIsInitialized's query-meta-class caching on a second call. Branches missed on
AbstractCriteriaBuilder dropped from 9 to 2; the 2 remaining (getMetaClass() meta-method
fast path in invokeMethod) were confirmed empirically unreachable via a dynamic-metaClass
test that passed without covering them, matching a prior assessment that this path needs
metaclass pollution for near-zero real value.

Also discovered (but did not fix, as it's pre-existing and untouched by this PR): calling
CriteriaBuilder.scroll() with zero args, or any argument Groovy's meta-method lookup
null-coerces to match scroll(Closure), causes infinite recursion between invokeMethod and
scroll() and a StackOverflowError. Worth a follow-up issue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scroll(Closure c) unconditionally re-entered invokeMethod(SCROLL_CALL, new Object[]{c})
to reuse the shared criteria-construction logic. When scroll() is called with the wrong
arg count/type (e.g. bare scroll(), or a call whose argument gets null-coerced by Groovy's
meta-method matching), isCriteriaConstructionMethod() correctly rejects it, but invokeMethod
then falls through to its getMetaClass() meta-method lookup, which is willing to match
scroll(Closure) with a null argument and invoke it - re-entering scroll() with the same null
argument forever, and StackOverflowing.

Extracted the shared criteria-construction logic (evaluate the closure, run the query,
reset state) into a new executeCriteriaConstruction() method, and changed scroll() to call
it directly instead of bouncing back through the dynamic dispatch in invokeMethod. This
closes the recursion path entirely: a malformed scroll() call now just runs the query with
no criteria applied, matching how an empty/null closure already behaves everywhere else in
this class, instead of crashing.

Found and root-caused while extending the SCROLL_CALL branch's test coverage for PR #16140;
CriteriaBuilder.java is already one of that PR's changed files even though this particular
method wasn't touched by its diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…CriteriaBuilder

Resolves an add/add conflict on grails-datamapping-rx CriteriaBuilderSpec.groovy:
both branches independently added a spec for this class. Keeps the
Book/newCriteria() style already established on feat/enable-datamapping-rx and
folds in the one complementary test case from this branch that exercises a
distinct code path -- findAll() applying pre-populated orderEntries via
prepareQuery(), as opposed to the closure-driven order() call covered by the
existing test.
@testlens-app

testlens-app Bot commented Aug 13, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: db5a42e
▶️ Tests: 45060 executed
⚪️ Checks: 55/55 completed


Learn more about TestLens at testlens.app.

Base automatically changed from feat/enable-datamapping-rx to 8.1.x August 15, 2026 02:34
@borinquenkid

Copy link
Copy Markdown
Member Author

@jdaugherty Did you intend to review this?

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants