Skip to content

SUM()/AVG() over an INTEGER column silently overflow: Type.increment's long-upgrade guard widens after the int overflow, and ignores negative overflow #5906

Description

@ruispereira

Summary

SUM() and AVG() over an integer column silently return wrong results once the running sum exceeds Integer.MAX_VALUE. Type.increment(), which the aggregators accumulate through, overflows in int arithmetic — and its own "upgrade to long" guard is broken, so it does not prevent the overflow it was written to catch. Negative overflow is not handled at all.

Version: 26.8.1. Type.increment() in engine/schema/Type.java, via SQLFunctionAverage / SQLFunctionSum.

Reproducer

CREATE VERTEX TYPE V;
CREATE PROPERTY V.n INTEGER;
-- five rows of 2,000,000,000; true sum = 10,000,000,000 (fits in long, not int)
INSERT INTO V SET n = 2000000000;   -- x5
SELECT sum(n) AS s, avg(n) AS a, count(n) AS c FROM V;
s = 5705032704        -- expected 10000000000
a = 1.1410065408E9    -- expected 2.0E9
c = 5

Both SUM and AVG are wrong by a wide margin, no error.

Root cause — the long-upgrade guard is a no-op

// engine/schema/Type.java:772
public static Number increment(final Number a, final Number b) {
  ...
  case Integer i -> {
    case Integer integer -> {
      final int sum = a.intValue() + b.intValue();               // overflows in int
      if (sum < 0 && a.intValue() > 0 && b.intValue() > 0)
        // SPECIAL CASE: UPGRADE TO LONG
        return (long) (a.intValue() + b.intValue());             // BUG: same int overflow, then widened
      return sum;
    }

The guard recomputes a.intValue() + b.intValue() — the same overflowing int addition — and only then casts to long. (long)(2000000000 + 2000000000) is (long)(-294967296), i.e. -294967296L, not 4000000000L. The widen happens after the damage. It should widen the operands first:

return (long) a.intValue() + (long) b.intValue();

Measured directly on Type.increment:

call result correct
increment(2000000000, 2000000000) -294967296 4000000000
increment(-2000000000, -2000000000) 294967296 -4000000000
increment(Integer.MIN_VALUE, -1) 2147483647 -2147483649

The second and third rows show the guard also ignores negative overflow entirely: its condition is sum < 0 && a > 0 && b > 0, so two large negatives (sum wraps positive) slip through unchecked. The Short branch has the identical shape and the identical two bugs.

Why it matters

  • SUM/AVG are among the most-used aggregates, and an integer column summing past 2^31 is ordinary (counts, sizes, amounts). The result is silently wrong, with no error and no hint — a reporting/analytics correctness failure.
  • The avg case compounds it: 1.14e9 vs a true 2.0e9 is not an obvious "too big" number, so it can pass a sanity check and be trusted.
  • Distinct from the store-time truncation (filed separately): here the stored values are fine, the aggregation arithmetic is what overflows.

Suggested fix

Widen before adding in every integral branch of increment ((long) a + (long) b, then decide whether the result still fits), and detect overflow symmetrically for positive and negative — Math.addExact throwing ArithmeticException on overflow, caught to trigger the long upgrade, is the clean primitive. The same applies to the Short/Byte branches.

A property test summing a column of Integer.MAX_VALUE-ish values and comparing to a long/BigDecimal reference would lock this in; the existing avg/sum tests likely pass because they mix in a long or double value, which routes through the (correct) widening branches rather than the Integer + Integer one.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions