Summary
BinaryComparator.compare() mishandles a value whose type is narrower than the operand it is compared against. When the first operand's type is INT, SHORT or BYTE, the comparator narrows the other operand to that same width regardless of its real type, and parses a string operand with Integer/Short/Byte.parseXxx. Two concrete consequences:
- Reachable via SQL, demonstrated: comparing a numeric field against a non-numeric string throws
NumberFormatException. SELECT ... WHERE n < 'abc' on an INTEGER column crashes the query.
- Comparator-contract violation, API-level: an
INT value compared against a LONG outside int range gives an inconsistent (non-antisymmetric) result, because the long operand is truncated with .intValue().
Version: 26.8.1. engine/serializer/BinaryComparator.java.
The code
The TYPE_INT branch (the SHORT and BYTE branches at lines 113 and 221 are identical in shape):
// BinaryComparator.java:49
final int v1 = ((Number) value1).intValue();
final int v2;
switch (type2) {
case TYPE_LONG:
case TYPE_DOUBLE:
case TYPE_FLOAT:
...
v2 = ((Number) value2).intValue(); // (1) narrows the OTHER operand to int
break;
case TYPE_STRING:
v2 = Integer.parseInt((String) value2); // (2) throws on a non-numeric string
break;
...
}
return Integer.compare(v1, v2);
Same pattern with shortValue() / Short.parseShort (line 126/134) and byteValue() / Byte.parseByte (234/242). The SHORT branch truncates to 16 bits and the BYTE branch to 8, so they are more lossy than the INT case.
1. NumberFormatException — demonstrated via SQL
CREATE VERTEX TYPE V;
CREATE PROPERTY V.n INTEGER;
INSERT INTO V SET n = 10;
SELECT n FROM V WHERE n < 'abc';
NumberFormatException: For input string: "abc"
at java.base/java.lang.Integer.parseInt(...)
at com.arcadedb.serializer.BinaryComparator.compare(BinaryComparator.java:70)
A numeric-vs-string comparison should either be a defined ordering or a clean typed error; instead it is an unchecked NumberFormatException, which over HTTP is a 500 rather than a 400. WHERE n = '10' works (the string parses), so the failure depends on the string's content — a query that is fine for one row's value crashes for another.
2. Antisymmetry violation — API-level, impact not demonstrated end-to-end
Driving compare() directly:
compare(-5, TYPE_INT, 2147483648L, TYPE_LONG) // returns 1 (-5 > 2147483648 ?!)
compare(2147483648L, TYPE_LONG, -5, TYPE_INT) // returns 1 (2147483648 > -5, correct)
Both return "greater", so the relation is not antisymmetric. The cause is (1): 2147483648L.intValue() overflows to -2147483648, and -5 > -2147483648. The TYPE_LONG branch (widening to long) is correct; only the narrow-first branches are wrong.
Honest scope: I could not turn this into a wrong SQL result. SELECT n FROM V WHERE n < 2147483648 returned the correct rows, so the query engine appears to normalise operand types (or operand order) before reaching the comparator. What is demonstrated is a broken comparator contract, not a wrong query. It matters because compare() is used for index key ordering and is a general-purpose entry point: any caller that invokes it directly on raw typed values — or any path that reaches the SHORT/BYTE branches, which truncate far more aggressively — inherits a non-total order. A comparator that is not a total order can also trip TimSort's "Comparison method violates its general contract" on the JDK sort path.
Suggested fix
Compare by widening, never by narrowing. Promote both operands to the widest of the two numeric types (or to double/BigDecimal for mixed integer/floating comparisons) before comparing, so no operand is truncated:
// instead of narrowing value2 to value1's width:
if (both integral) return Long.compare(((Number)value1).longValue(), ((Number)value2).longValue());
For the string case, either define the ordering (parse and fall back to a typed comparison error rather than a raw NFE) or reject the comparison with a CommandExecutionException naming the two types, so it surfaces as a 400. Catching the NumberFormatException and mapping it is the minimum; widening is the real fix for the numeric half.
Summary
BinaryComparator.compare()mishandles a value whose type is narrower than the operand it is compared against. When the first operand's type isINT,SHORTorBYTE, the comparator narrows the other operand to that same width regardless of its real type, and parses a string operand withInteger/Short/Byte.parseXxx. Two concrete consequences:NumberFormatException.SELECT ... WHERE n < 'abc'on anINTEGERcolumn crashes the query.INTvalue compared against aLONGoutside int range gives an inconsistent (non-antisymmetric) result, because the long operand is truncated with.intValue().Version: 26.8.1.
engine/serializer/BinaryComparator.java.The code
The
TYPE_INTbranch (theSHORTandBYTEbranches at lines 113 and 221 are identical in shape):Same pattern with
shortValue()/Short.parseShort(line 126/134) andbyteValue()/Byte.parseByte(234/242). The SHORT branch truncates to 16 bits and the BYTE branch to 8, so they are more lossy than the INT case.1. NumberFormatException — demonstrated via SQL
A numeric-vs-string comparison should either be a defined ordering or a clean typed error; instead it is an unchecked
NumberFormatException, which over HTTP is a 500 rather than a 400.WHERE n = '10'works (the string parses), so the failure depends on the string's content — a query that is fine for one row's value crashes for another.2. Antisymmetry violation — API-level, impact not demonstrated end-to-end
Driving
compare()directly:Both return "greater", so the relation is not antisymmetric. The cause is
(1):2147483648L.intValue()overflows to-2147483648, and-5 > -2147483648. TheTYPE_LONGbranch (widening tolong) is correct; only the narrow-first branches are wrong.Honest scope: I could not turn this into a wrong SQL result.
SELECT n FROM V WHERE n < 2147483648returned the correct rows, so the query engine appears to normalise operand types (or operand order) before reaching the comparator. What is demonstrated is a broken comparator contract, not a wrong query. It matters becausecompare()is used for index key ordering and is a general-purpose entry point: any caller that invokes it directly on raw typed values — or any path that reaches theSHORT/BYTEbranches, which truncate far more aggressively — inherits a non-total order. A comparator that is not a total order can also tripTimSort's "Comparison method violates its general contract" on the JDK sort path.Suggested fix
Compare by widening, never by narrowing. Promote both operands to the widest of the two numeric types (or to
double/BigDecimalfor mixed integer/floating comparisons) before comparing, so no operand is truncated:For the string case, either define the ordering (parse and fall back to a typed comparison error rather than a raw NFE) or reject the comparison with a
CommandExecutionExceptionnaming the two types, so it surfaces as a 400. Catching theNumberFormatExceptionand mapping it is the minimum; widening is the real fix for the numeric half.