Skip to content

[Cherry-pick to branch-1.3] [#12598] fix(common): Reject out-of-range numeric statistic values (#12599) - #12874

Merged
roryqi merged 1 commit into
apache:branch-1.3from
qqqttt123:cherry-pick-09e68277-to-branch-1.3
Sep 3, 2026
Merged

[Cherry-pick to branch-1.3] [#12598] fix(common): Reject out-of-range numeric statistic values (#12599)#12874
roryqi merged 1 commit into
apache:branch-1.3from
qqqttt123:cherry-pick-09e68277-to-branch-1.3

Conversation

@roryqi

@roryqi roryqi commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Cherry-pick commit 09e68277b2d8df18b397c2373a2ada2aef8ce04f from #12599 to branch-1.3.

This change:

  • Rejects integral statistic values that cannot be represented as a signed 64-bit integer.
  • Rejects floating-point statistic values that parse to a non-finite double.
  • Adds tests for boundaries, nested values, and statistics update request deserialization.

Why are the changes needed?

Out-of-range numeric statistic values are silently converted and stored as different values in 1.3. For example, an integer larger than Long.MAX_VALUE wraps to a negative value, while an oversized floating-point value becomes Infinity.

This backport prevents silent statistic corruption by rejecting these values.

Backport: #12599

Does this PR introduce any user-facing change?

Yes. Out-of-range numeric statistic values now result in a 400 Bad Request instead of returning success and storing an altered value.

No API signatures, properties, or stored-data formats are changed.

How was this patch tested?

./gradlew :common:test --tests org.apache.gravitino.json.TestJsonUtils :common:javadoc :common:spotlessCheck -PskipITs

…ues (apache#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: apache#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.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Code Coverage Report

Overall Project 68.19% +0.14% 🟢
Files changed 85.02% 🟢

Module Coverage
aliyun 1.72% 🔴
api 47.7% 🟢
authorization-common 85.96% 🟢
aws 42.04% 🟢
azure 2.47% 🔴
catalog-common 10.4% 🔴
catalog-fileset 80.23% 🟢
catalog-glue 68.95% 🟢
catalog-hive 79.42% 🟢
catalog-jdbc-clickhouse 84.7% 🟢
catalog-jdbc-common 45.48% 🟢
catalog-jdbc-doris 82.61% 🟢
catalog-jdbc-hologres 54.03% 🟢
catalog-jdbc-mysql 79.33% 🟢
catalog-jdbc-oceanbase 78.6% 🟢
catalog-jdbc-postgresql 83.39% 🟢
catalog-jdbc-starrocks 79.16% 🟢
catalog-kafka 77.01% 🟢
catalog-lakehouse-generic 59.18% 🟢
catalog-lakehouse-hudi 79.1% 🟢
catalog-lakehouse-iceberg 85.86% 🟢
catalog-lakehouse-paimon 82.14% 🟢
catalog-model 77.72% 🟢
cli 44.51% 🟢
client-java 78.16% 🟢
common 50.81% +4.64% 🟢
core 83.01% 🟢
filesystem-hadoop3 77.3% 🟢
flink 0.0% 🔴
flink-common 49.22% 🟢
flink-runtime 0.0% 🔴
gcp 14.12% 🔴
hadoop-common 10.88% 🔴
hive-metastore-common 53.77% 🟢
iceberg-aliyun-bundle 0.0% 🔴
iceberg-common 58.15% 🟢
iceberg-rest-server 73.87% 🟢
idp-basic 86.02% 🟢
integration-test-common 0.0% 🔴
jobs 66.17% 🟢
lance-common 24.42% 🔴
lance-rest-server 60.13% 🟢
lineage 53.02% 🟢
optimizer 82.95% 🟢
optimizer-api 21.95% 🔴
server 86.87% 🟢
server-common 76.28% 🟢
spark 28.57% 🔴
spark-common 41.66% 🟢
trino-connector 47.35% 🟢
Files
Module File Coverage
common JsonUtils.java 85.02% 🟢

@roryqi
roryqi merged commit 4d7f636 into apache:branch-1.3 Sep 3, 2026
35 checks passed
@roryqi
roryqi deleted the cherry-pick-09e68277-to-branch-1.3 branch September 3, 2026 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants