rzo1 commented on code in PR #144:
URL: https://github.com/apache/openjpa/pull/144#discussion_r3563619991


##########
openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/functions/TestEJBQLFunction.java:
##########


Review Comment:
   Had a look at this one. I debugged it by replaying the generated SQL against 
a plain in-memory HSQLDB (`jdbc:hsqldb:mem:`) from a small standalone `main()` 
and comparing the error codes / results of different cast variations:
   
   ```
   ROUND(..., CAST(? AS DOUBLE))   -> -5563 incompatible data type in operation
   ROUND(..., CAST(? AS INTEGER))  -> works
   ROUND(..., ?)                   -> works
   ```
   
   So your guess is exactly right: HSQLDB insists on an integer scale for 
`ROUND`.
   
   Where the `CAST(? AS DOUBLE)` comes from: `HSQLDictionary` sets 
`requiresCastForMathFunctions = true`, and `DBDictionary.mathFunction()` then 
promotes *both* arguments to the common numeric type (`Filters.promote(Double, 
Integer)` → `double`) — even though the kernel (`Math` constructor) had already 
typed the scale argument as `Integer`.
   
   There is a second problem hiding behind it though: with only the scale cast 
fixed, the query returns `21.0` instead of the expected `21.857`. The inner 
`CAST(SUM(t0.age) AS NUMERIC)` is the culprit — in HSQLDB, `NUMERIC` without 
precision/scale means **scale 0**, and the bare `?` divisor infers its type 
from the other operand, so `7.0` is truncated to `7` and the division is 
evaluated with scale 0:
   
   ```
   CAST(SUM(age) AS NUMERIC) / ?          -> 21
   CAST(SUM(age) AS NUMERIC(40,20)) / ?   -> 21.857142857142857...
   ```
   
   Maybe something like this works — in `DBDictionary.mathFunction()`, keep a 
separate cast type for the ROUND scale:
   
   ```java
           int type = 0;
           int rtype = 0;
           if (requiresCastForMathFunctions && (lc != rc
               || (lhs.isConstant() || rhs.isConstant()))) {
               Class c = Filters.promote(lc, rc);
               type = getJDBCType(JavaTypes.getTypeCode(c), false);
               rtype = type;
               if (type != Types.VARBINARY && type != Types.BLOB) {
                   castlhs = (lhs.isConstant() && rhs.isConstant()) || lc != c;
                   castrhs = (lhs.isConstant() && rhs.isConstant()) || rc != c;
               }
           }
   
           boolean mod = "MOD".equals(op);
           boolean power = "POWER".equals(op);
           boolean round = "ROUND".equals(op);
           if (round && castrhs) {
               // the second argument of ROUND is an integer scale; casting it 
to
               // the promoted numeric type of the first argument produces e.g.
               // ROUND(x, CAST(? AS DOUBLE)), which databases reject
               rtype = Types.INTEGER;
           }
   ```
   
   (and `appendCast(buf, rhs, rtype)` instead of `type` at the bottom of the 
method)
   
   plus in `HSQLDictionary` (`appendLength` is only ever called from 
`appendCast`, so this does not affect DDL):
   
   ```java
       @Override
       protected void appendLength(SQLBuffer buf, int type) {
           // HSQLDB gives CAST(x AS NUMERIC) without precision and scale a 
scale
           // of 0 and silently truncates all fractional digits, so casts 
written
           // for requiresCastForMathFunctions / requiresCastForComparisons 
would
           // corrupt decimal values; always spell out precision and scale.
           // 40/20 is an arbitrary but generous choice - adjust if needed
           if (type == Types.NUMERIC || type == Types.DECIMAL) {
               buf.append("(40,20)");
           } else {
               super.appendLength(buf, type);
           }
       }
   ```
   
   With both changes `TestEJBQLFunction` on HSQLDB is down to a single 
remaining error (`testExtractWEEK` — HSQLDB does not know `EXTRACT(WEEK ...)`, 
it wants `WEEK_OF_YEAR`, that one needs a separate field-name mapping), and 
Derby stays green (64/64).
   



##########
openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/functions/TestEJBQLFunction.java:
##########


Review Comment:
   This one turned out to have the same root cause as the wrong ROUND *value* 
(see the other comment). Debugged it the same way — standalone `main()` against 
an in-memory HSQLDB, comparing variations:
   
   ```
   SELECT 24 - 0.4                                       -> 23.6   (inline 
literals)
   SELECT ? - ?                    (24, 0.4)             -> 24  (!)
   SELECT CAST(? AS NUMERIC) - ?   (24, 0.4)             -> 24  (!)   <- this 
is what we generate
   SELECT CAST(? AS NUMERIC(30,10)) - CAST(? AS ...)     -> 23.6
   ```
   
   OpenJPA renders JPQL literals as prepared-statement parameters, so the 
inline form never reaches the DB. For `24 - 0.4`, `mathFunction()` promotes 
`Long`/`BigDecimal` to `BigDecimal` and emits `CAST(? AS NUMERIC) - ?`. On 
HSQLDB that goes wrong twice:
   
   1. `CAST(? AS NUMERIC)` without precision/scale is `NUMERIC` with **scale 
0** → `24` stays `24`, and
   2. the bare `?` infers its type from the other operand — also `NUMERIC` 
scale 0 — so `0.4` is truncated to `0`.
   
   Result: `24 - 0 = 24`. So the `- 0.4` isn't ignored, it's truncated :)
   
   The `appendLength` override from the ROUND comment fixes this one as well: 
the cast becomes `CAST(? AS NUMERIC(40,20)) - ?`, the bare parameter inherits 
the scaled type, and the result is `23.6`. The `CEILING`/`FLOOR` tests 
(`SUM(c.age) + 0.4` etc.) are covered by the same fix.
   
   Re "how can this be debugged": I set `openjpa.Log=SQL=TRACE` to capture the 
generated SQL, then replayed it 1:1 in a small `main()` against 
`jdbc:hsqldb:mem:` and mutated the casts until the error code / wrong value 
pointed at the responsible piece.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to