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.
Summary
SUM()andAVG()over an integer column silently return wrong results once the running sum exceedsInteger.MAX_VALUE.Type.increment(), which the aggregators accumulate through, overflows inintarithmetic — 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()inengine/schema/Type.java, viaSQLFunctionAverage/SQLFunctionSum.Reproducer
Both
SUMandAVGare wrong by a wide margin, no error.Root cause — the long-upgrade guard is a no-op
The guard recomputes
a.intValue() + b.intValue()— the same overflowingintaddition — and only then casts tolong.(long)(2000000000 + 2000000000)is(long)(-294967296), i.e.-294967296L, not4000000000L. The widen happens after the damage. It should widen the operands first:Measured directly on
Type.increment:increment(2000000000, 2000000000)-2949672964000000000increment(-2000000000, -2000000000)294967296-4000000000increment(Integer.MIN_VALUE, -1)2147483647-2147483649The 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. TheShortbranch has the identical shape and the identical two bugs.Why it matters
SUM/AVGare 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.avgcase compounds it:1.14e9vs a true2.0e9is not an obvious "too big" number, so it can pass a sanity check and be trusted.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.addExactthrowingArithmeticExceptionon overflow, caught to trigger the long upgrade, is the clean primitive. The same applies to theShort/Bytebranches.A property test summing a column of
Integer.MAX_VALUE-ish values and comparing to along/BigDecimalreference would lock this in; the existing avg/sum tests likely pass because they mix in alongordoublevalue, which routes through the (correct) widening branches rather than theInteger + Integerone.