LuciferYang opened a new pull request, #12599:
URL: https://github.com/apache/gravitino/pull/12599

   ### What changes were proposed in this pull request?
   
   Add two range guards to `JsonUtils.getStatisticValue`:
   
   - integral branch: `JsonNode.canConvertToLong()` before `asLong()`
   - floating-point branch: `Double.isFinite()` on the result of `asDouble()`
   
   Both reject through `Preconditions.checkArgument`, matching how the other 
read paths in this file signal bad input (`readFunctionArg` and the partition 
reader both throw `IllegalArgumentException`).
   
   The change also drops the checked-exception plumbing around the terminal 
branch. It threw `UnsupportedEncodingException`, a character-encoding error 
used to report a bad JSON node type, which forced `getStatisticValue` to 
declare `throws IOException`, which in turn forced the object branch to launder 
that exception out of a lambda through a bare `RuntimeException`. None of it 
was ever reachable, before or after this change: JSON text can only produce 
node types the method already handles. The branch is reachable through 
`ObjectMapper.convertValue` with an embedded binary node, which is what the new 
test for it uses. It now throws `IllegalArgumentException`, and the `throws` 
clause and the `try/catch` are gone.
   
   Two `if (value != null)` checks in the recursive branches are removed as 
well. `getStatisticValue` never returns null: every branch either returns a 
`StatisticValues` instance or throws.
   
   ### Why are the changes needed?
   
   A statistic value past the 64-bit range was silently replaced by a different 
number, with the sign flipped in some cases. `9223372036854775808` was stored 
as `-9223372036854775808`, and `123456789012345678901234567890` as 
`-4362896299872285998`. An out-of-range floating-point literal became 
`Infinity`, which the serializer writes back out as the JSON string 
`"Infinity"`, so the value returned as a `StringValue` on the next round trip.
   
   `StatisticsUpdateRequest.validate()` cannot catch this: it only checks for a 
null value, and it runs after Jackson has built the map, by which point the 
truncated `long` is all that is left. The deserializer is the only place where 
the information needed to detect the loss still exists.
   
   `StatisticValue` has no BigInteger or BigDecimal type, so there is no 
lossless representation to fall back to, and failing the request is better than 
storing a wrong number.
   
   Fix: #12598
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes. `PUT /metalakes/{metalake}/objects/{type}/{fullName}/statistics` and 
its `/partitions` variant now return 400 for a numeric statistic value outside 
the `long` range, or a floating-point value that is not finite. They previously 
returned 200 and stored a wrong number.
   
   No stored data becomes unreadable. The serializer can only emit in-range 
longs and finite doubles (a non-finite double goes out as the quoted string 
`"Infinity"`), so nothing already persisted trips the new guards. Rows already 
corrupted by the old behaviour keep their wrong value; this change does not 
repair them.
   
   No API signatures, property keys, or configuration change.
   
   ### How was this patch tested?
   
   Four test methods in `TestJsonUtils`. All four were confirmed to fail 
against the pre-fix code by reverting `JsonUtils.java` and re-running, not by 
inspection:
   
   - `testStatisticValueRejectsOutOfRangeIntegral` — both 64-bit boundaries are 
still accepted; one past each boundary and two far outside are rejected, 
including nested inside a list and inside an object.
   - `testStatisticValueRejectsNonFiniteFloatingPoint` — `±1.5E400`, asserting 
the full message including the rendered `Infinity` / `-Infinity`. The message 
reports the parsed double rather than echoing the literal, because the node 
Jackson hands the deserializer already holds the infinity.
   - `testStatisticsUpdateRequestRejectsOutOfRangeValue` — the real 
request-body shape, where the value is `Map` content and Jackson wraps the 
rejection into `JsonMappingException`, which the server maps to 400. It uses a 
bare `ObjectMapper` so the assertion rests on the DTO's 
`@JsonDeserialize(contentUsing = ...)` annotation rather than on a module the 
test registered; removing that annotation makes the test fail.
   - `testStatisticValueRejectsUnsupportedNodeType` — the terminal branch. It 
asserts `assertNull(e.getCause())`, because `ObjectMapper.convertValue` 
relaunders a deserializer `IOException` into an `IllegalArgumentException` 
carrying the same message, so only the cause distinguishes our own rejection 
from the old checked exception.
   
   ```
   ./gradlew :common:test :core:test :server:test :common:javadoc 
:common:spotlessCheck -PskipITs
   ```
   
   passes.
   
   Follow-ups found while working on this, not included here to keep the change 
to one concern:
   
   - `StatisticValues.doubleValue(double)` accepts `Infinity` and `NaN`, so the 
write side can still produce a value this change now refuses to read back as a 
double. The root fix belongs in `api` and carries its own compatibility 
discussion.
   - `PartitionStatisticsUpdateDTO.validate()` has no per-entry null check, 
unlike `StatisticsUpdateRequest.validate()`. Jackson's `MapDeserializer` does 
not invoke a `contentUsing` deserializer for a `VALUE_NULL` content token, so a 
top-level JSON null reaches storage on that route.
   - `JdbcPartitionStatisticStorage.parseResultSet` catches 
`JsonProcessingException` to log the partition and statistic name; an unchecked 
`IllegalArgumentException` bypasses that handler.
   


-- 
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