Skip to content

Resolve enum constants in UPDATE/DELETE queries without an identification variable - #2761

Open
renatsaf wants to merge 4 commits into
eclipse-ee4j:masterfrom
renatsaf:fix-issue-2758-enum-literal-update-delete
Open

renatsaf wants to merge 4 commits into
eclipse-ee4j:masterfrom
renatsaf:fix-issue-2758-enum-literal-update-delete

Conversation

@renatsaf

@renatsaf renatsaf commented Jun 6, 2026 •

Copy link
Copy Markdown

Problem

A bulk UPDATE/DELETE that does not declare an identification variable loses a fully qualified enum constant:

UPDATE Todo SET status = com.example.domain.Status.PENDING WHERE id = :id

generates UPDATE TODO SET STATUS = WHERE (ID = ?). The constant is lost wherever it appears in such a query (H2):

JPQL, no identification variable Before
SET status = <enum> SET STATUS = , invalid SQL
WHERE status = <enum> QueryException: Object comparisons can only be used with OneToOneMappings
WHERE status IN (<enum>) STATUS IN (), no row matched, no error
SET status = NULLIF(<enum>, status) NULLIF(, STATUS), invalid SQL

This blocks Jakarta Data @Query repository methods, whose JDQL omits aliases.

Root cause

FullyQualifyPathExpressionVisitor qualifies every path of such a query with a virtual this identification variable, the enum constant included. ExpressionBuilderVisitor then navigates com.example.domain.Status.PENDING from the entity: the first segment is not a mapping, path resolution stops, and the entity expression itself is used as the value. The existing enum-constant fallback (resolveEnumConstant) only runs when the identification variable does not resolve, which never happens for the virtual this.

Fix

In ExpressionBuilderVisitor.PathResolver, a path qualified with the implicit this whose first segment is neither a mapping nor a query key of the entity is resolved as an enum constant. The parse tree and validation are unchanged, and navigations such as address.city or room.doors are resolved against the entity as before.

The first commit of this PR changed FullyQualifyPathExpressionVisitor to leave multi-segment paths unqualified. As reported in review, that broke nested navigations (manager.phoneNumbers IS EMPTY at runtime, address.city in the tools validator): without mapping metadata the parser cannot tell a navigation from an enum constant. That commit is reverted.

Type resolution needs no change: TypeResolver and the tools ResolverBuilder already fall back to the enum type; each query was checked against its aliased form.

Tests

  • JUnitJPQLJakartaDataNoAliasTest (runtime): enum constant in SET, SET + WHERE, DELETE ... WHERE =, DELETE ... IN; room.status = <enum>; room.doors IS NOT EMPTY in UPDATE and DELETE.
  • AbstractSemanticValidatorTest: unqualified UPDATE/DELETE with managerEmployee.phoneNumbers IS EMPTY, address.city, embeddedAddress.city, shelfLife.soldDate, and enum constants in = and IN.
  • org.eclipse.persistence.jpa.jpql: 114263 tests, 0 failures. JUnitJPQLJakartaDataNoAliasTest on H2: 40/40; without the fix the 5 new enum tests fail. Full jpa.test.jpql on H2 with and without the fix: no other test changed status.

Fixes #2758

🤖 Generated with Claude Code

…E queries

A bulk UPDATE/DELETE without an identification variable (alias) such as

    UPDATE Todo SET status = com.example.domain.Status.PENDING WHERE id = :id

generated invalid SQL ("UPDATE TODO SET STATUS =  WHERE ...") because the
fully-qualified enum literal in the new value was silently dropped. This also
blocks Jakarta Data @query repository methods, whose JDQL omits aliases by
convention.

FullyQualifyPathExpressionVisitor qualifies unqualified path expressions of an
UPDATE/DELETE query with a virtual "this" identification variable. It did so
for every path, including the fully-qualified enum constant used as the new
value, turning "com.example.domain.Status.PENDING" into a path rooted at the
entity. The leading segment "com" then failed to resolve to a mapping, so the
enum constant was never built and the assigned value became empty.

Only single-segment, unqualified attributes (e.g. "status") are now qualified
with the virtual identification variable. Multi-segment paths are left as-is so
a fully-qualified enum constant is resolved as an enum literal, consistent with
how unqualified SELECT queries already handle them. The update target is still
always qualified (visit(UpdateItem)) so a relationship/embeddable navigation on
the left side keeps being resolved and validated against the entity.

Fixes eclipse-ee4j#2758

Signed-off-by: Renat R. Safiullin <renatsaf@gmail.com>

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@robertpatrick

Copy link
Copy Markdown
Contributor

@renatsaf I compiled the parser source in an isolated directory and compared the PR with the parent’s original visitor. These are executed grammar/semantic-validation results, without a database, and found the following issues:

  1. Unaliased DELETE with contact.phoneNumbers IS EMPTY: COLLECTION_VALUED_PATH_EXPRESSION_NOT_RESOLVABLE
  2. Unaliased UPDATE with the same predicate: Same failure
  3. Nested scalar predicate contact.city = 'x', through the generic tools validator: Field-resolution and undeclared-variable errors

Can you please have a look at the following recommendations?

  • Preserve qualification for collection-valued paths. They cannot represent enum literals.
  • Preserve existing nested scalar navigation while distinguishing enum literals.
  • Add focused regression tests for each of the failing cases

renatsaf and others added 3 commits September 13, 2026 22:08
…TE/DELETE queries"

This reverts commit 7fbd632.

Leaving multi-segment paths unqualified in FullyQualifyPathExpressionVisitor
broke nested navigations in UPDATE/DELETE queries without an identification
variable: "DELETE FROM Employee WHERE manager.phoneNumbers IS EMPTY" fails with
COLLECTION_VALUED_PATH_EXPRESSION_NOT_RESOLVABLE, and "address.city = 'x'" is
rejected by the tools validator. Without the mapping metadata the parser cannot
tell such a navigation from a fully qualified enum constant, so the enum
constant has to be resolved where that metadata is available.

Signed-off-by: Renat R. Safiullin <renatsaf@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion variable

An UPDATE or DELETE query that does not declare an identification variable
lost a fully qualified enum constant:

    UPDATE Todo SET status = com.example.domain.Status.PENDING WHERE id = :id

generated "UPDATE TODO SET STATUS =  WHERE (ID = ?)". The constant was lost in
any position: "status = <enum>" failed with an object comparison
QueryException, and "status IN (<enum>)" silently matched no rows.

FullyQualifyPathExpressionVisitor qualifies every path of such a query with a
virtual "this" identification variable, the enum constant included.
ExpressionBuilderVisitor then navigated the constant from the entity, stopped
at its first segment, which is not a mapping, and used the entity expression
as the value. The enum constant fallback only ran when the identification
variable could not be resolved.

A path qualified with the implicit "this" whose first segment is neither a
mapping nor a query key of the entity is now resolved as an enum constant.
Navigations such as "room.doors" are resolved against the entity as before.

Fixes eclipse-ee4j#2758

Signed-off-by: Renat R. Safiullin <renatsaf@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iable

Nested navigations (managerEmployee.phoneNumbers IS EMPTY, address.city,
embeddedAddress.city, shelfLife.soldDate) and fully qualified enum constants
in UPDATE and DELETE queries that do not declare an identification variable
must validate without problems. The previous attempt to fix eclipse-ee4j#2758 broke these
navigations without any test failing.

Signed-off-by: Renat R. Safiullin <renatsaf@gmail.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@renatsaf renatsaf changed the title Keep fully-qualified enum literals intact in unqualified UPDATE/DELETE queries Resolve enum constants in UPDATE/DELETE queries without an identification variable Sep 13, 2026
@renatsaf

Copy link
Copy Markdown
Author

@robertpatrick Confirmed. All three reproduce with the tools validators, and 1 and 2 also fail at runtime (JPQLException: The collection-valued path 'owner.phoneNumbers' cannot be resolved to a valid association field). Case 3 passes at runtime only because the core semantic validator retries an unresolvable state field path with the implicit this and rewrites it; the tools validator does not.

Keeping the qualification for collection-valued paths alone would still leave case 3, and the underlying problem remains: without mapping metadata the parser cannot tell address.city from com.example.Status.DONE. The parser change is therefore reverted (543112e), and the enum constant is resolved in ExpressionBuilderVisitor, where the descriptor is available: a path qualified with the implicit this whose first segment is neither a mapping nor a query key of the entity is resolved as an enum constant (a2c9db5). Navigations are qualified and resolved exactly as before.

Regression tests for each case:

  • AbstractSemanticValidatorTest.test_ValidQuery_06 / _07: managerEmployee.phoneNumbers IS EMPTY in DELETE / UPDATE
  • AbstractSemanticValidatorTest.test_ValidQuery_08 - _10: address.city, embeddedAddress.city, shelfLife.soldDate
  • JUnitJPQLJakartaDataNoAliasTest.testUpdateQueryImplicitThisVariableAndCollectionValuedNavigation, testDeleteQueryImplicitThisVariableAndCollectionValuedNavigation, testUpdateQueryImplicitThisVariableAndEnumInNavigation (runtime)

Running the queries against H2 also showed the original defect is not limited to SET: WHERE status IN (<enum>) matched no rows without an error, and WHERE status = <enum> failed with an object-comparison QueryException. Both are covered by the new runtime tests.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JPQL bulk UPDATE drops fully-qualified enum literal in SET clause when target field is unqualified (no alias)

4 participants