Skip to content

Convert enum literals and parameters inside CASE, COALESCE and NULLIF - #2850

Open
renatsaf wants to merge 1 commit into
eclipse-ee4j:masterfrom
renatsaf:fix-issue-2848-enum-in-function-arguments
Open

renatsaf wants to merge 1 commit into
eclipse-ee4j:masterfrom
renatsaf:fix-issue-2848-enum-in-function-arguments

Conversation

@renatsaf

Copy link
Copy Markdown

Problem

A fully qualified enum literal compared with an attribute is converted through the attribute's mapping (t.status = com.example.Status.DONE binds DONE for @Enumerated(STRING)). The same literal, or an enum parameter, used as an argument of CASE, COALESCE or NULLIF is written as its ordinal or as its bare name (H2):

JPQL Before
UPDATE Todo t SET t.status = CASE WHEN t.id = 1 THEN <DONE> ELSE <PENDING> END THEN 1 ELSE 0, stores '1'/'0' without error
UPDATE Todo t SET t.status = COALESCE(NULLIF(t.status, t.status), <DONE>) binds 1, stores '1' without error
UPDATE Todo t SET t.status = CASE t.id WHEN 1 THEN <DONE> ELSE t.status END THEN DONE, unknown column
UPDATE Todo t SET t.status = NULLIF(t.status, <DONE>) binds 1, data conversion error
SELECT t FROM Todo t WHERE t.status = CASE WHEN t.id = 1 THEN <PENDING> ELSE <DONE> END THEN 0 ELSE 1, conversion error or wrong rows
SELECT t FROM Todo t WHERE COALESCE(t.status, <PENDING>) = <DONE> binds 0, 1
SELECT t FROM Todo t WHERE NULLIF(t.status, <DONE>) IS NULL binds 1
SELECT t FROM Todo t WHERE :p = CASE WHEN t.id = 1 THEN t.status ELSE :p END binds 1

The Criteria API is affected the same way (cb.coalesce, cb.selectCase, cb.nullif).

Root cause

A constant or parameter is converted by localBase.getFieldValue(); with the ExpressionBuilder as local base, Expression.getFieldValue() turns an enum into its ordinal. Only direct operands get the attribute as local base: Expression.from() for a comparison and ExpressionQueryMechanism.prepareUpdateAll() for UPDATE SET (value expressions only). For the functions:

  • Expression.from() never passes the attribute to a function operand, and FunctionExpression.setLocalBase() only forwards to the first child.
  • UPDATE SET skips a function value.
  • FunctionExpression.create() gives every function argument the ExpressionBuilder as local base, so NULLIF does not convert its second argument like its first.
  • Without anything on the other side (COALESCE(t.status, <PENDING>) = <DONE>), nothing converts the literal inside the function, and the value compared with the function is converted through the function, which has no mapping.

Fix

FunctionExpression, for CASE, COALESCE and NULLIF (functions whose value is one of their arguments):

  • setLocalBase() passes the attribute down to the result arguments (THEN/ELSE results, all COALESCE arguments, both NULLIF arguments).
  • getFieldValue() converts through the first attribute result argument.
  • normalize() converts a constant or parameter result argument that has no attribute local base through the function's own attribute argument. It is done when normalizing because CASE and COALESCE arguments are added one at a time.
  • create(): a constant or parameter NULLIF argument is converted through the other argument when that one is an attribute.

Expression.from() passes the attribute to such a function operand, and ExpressionQueryMechanism sets the local base of such a function assigned in UPDATE SET.

Only an attribute (a query key, a field, or the first attribute result argument of such a function) is ever used as local base. Passing a value down lets local bases reference each other: an early version of this change failed TestQuerySyntaxComparisonTests.testNullIf2_* (?1 = NULLIF(?2, s.strVal1)) with a StackOverflowError in ParameterExpression.getBuilder(), the cycle noted in Expression.from() (3107049). Simple CASE WHEN values keep the operand as local base.

Tests

  • New TestQueryEnumFunctionArguments (jse, 14 tests): JPQL UPDATE SET with searched CASE, simple CASE, COALESCE (literal and parameter) and NULLIF; SELECT with CASE, COALESCE on the left of the comparison, NULLIF ... IS NULL, a value on both sides of NULLIF and of CASE; Criteria coalesce, selectCase, nullif in an update. Updates are checked by reading the STATUS column with a native query.
  • Derby, this branch with and without the fix: core unit tests (12), eclipselink.core.test SRG (1243), testapps jpql (613), criteria (329), advanced (442), advanced2 (159), eclipselink.jpa.test.jse (783, 33 skipped, including TestQueryCase, TestQueryCoalesce, TestQuerySyntaxComparisonTests, TestCoalesceFunction, TestSimpleCase, TestParameterBinding). Without the fix the 14 new tests fail (9 wrong values or rows, 5 Derby type errors); with the fix all pass, and no other test changed status.

Not covered

These were listed in #2848 but do not depend on the enum mapping; they happen with a String attribute as well:

  • H2 rejects COALESCE(?, ?) (Unknown data type: "?"), e.g. t.status = COALESCE(:p, <DONE>). The values are now bound converted.
  • In UPDATE ... SET x = COALESCE(:p, ...), getParameterType() of :p is null.
  • A SELECT projection of CASE/COALESCE over an enum attribute returns the column value (String), not the enum.

Fixes #2848

🤖 Generated with Claude Code

A constant or parameter is converted through the mapping of its local base.
Only a direct operand of a comparison or of an UPDATE SET got the attribute
as local base, so an argument of CASE, COALESCE or NULLIF kept the
ExpressionBuilder and an enum was bound as its ordinal, or printed as its
bare name, even for @Enumerated(STRING) and converted attributes.

- FunctionExpression: CASE, COALESCE and NULLIF pass the attribute they
  are compared with or assigned to down to their result arguments, convert
  a value compared with them through their attribute argument, and convert
  a remaining constant or parameter result argument through that attribute
  when normalizing. NULLIF converts a value argument through its attribute
  argument. Only an attribute is used as local base, so local bases cannot
  reference each other.
- Expression.from: pass the attribute to a CASE, COALESCE or NULLIF value.
- ExpressionQueryMechanism: also set the local base of such a function
  assigned in UPDATE SET.

Fixes eclipse-ee4j#2848

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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: enum literal or parameter inside CASE, COALESCE or NULLIF ignores the attribute's enum mapping

2 participants