fix: reject precision-losing int in JSON 'in' with mixed types

Mixing an int64 literal beyond the safe float64 range (|v| > 2^53) with
any float literal in a JSON 'in' list used to coerce every integer to
float64 in the parser, so distinct int64 values collapsed to the same
IEEE-754 double and matched each other incorrectly. Reject such queries
at parse time and add a matching guard in the JSON numeric scalar-index
path so the same class of silent mismatch cannot reappear when a scalar
index is present.

issue: #48442
Signed-off-by: Buqian Zheng <zhengbuqian@gmail.com>
This commit is contained in:
Buqian Zheng
2026-04-22 09:12:49 +00:00
parent e91e8825f7
commit 1854c909cd
4 changed files with 68 additions and 3 deletions
+15 -2
View File
@@ -918,8 +918,21 @@ PhyTermFilterExpr::ExecVisitorImplForIndex() {
for (auto& val : expr_->vals_) {
if constexpr (std::is_same_v<T, double>) {
if (val.has_int64_val()) {
// only json field will cast int to double because other fields are casted in proxy
vals.emplace_back(static_cast<double>(val.int64_val()));
// only json field will cast int to double because other fields are casted in proxy.
// Reject values outside the safe float64 range (|v| > 2^53) to
// avoid distinct int64 values collapsing to the same double and
// producing incorrect matches via the numeric scalar index.
auto iv = val.int64_val();
constexpr int64_t kMaxSafeDouble = int64_t{1} << 53;
if (iv > kMaxSafeDouble || iv < -kMaxSafeDouble) {
ThrowInfo(DataTypeInvalid,
"cannot compare JSON numeric field against "
"integer {} via the numeric scalar index: "
"value exceeds the safe float64 range "
"(|v| <= 2^53) and would lose precision",
iv);
}
vals.emplace_back(static_cast<double>(iv));
continue;
}
}
@@ -759,8 +759,16 @@ func (v *ParserVisitor) VisitTerm(ctx *parser.TermContext) interface{} {
hasFloat = true
}
}
// If we have both int and float, convert all ints to floats
// If we have both int and float, convert all ints to floats.
// Reject the query when any integer would lose precision under
// float64 (|v| > 2^53), otherwise distinct int64 values collapse
// to the same double and produce incorrect matches.
if hasInt && hasFloat {
for _, val := range values {
if IsInteger(val) && !fitsInFloat64Exactly(val.GetInt64Val()) {
return merr.WrapErrParameterInvalidMsg("cannot mix integer %d with float in JSON 'in' list: value exceeds the safe float64 range (|v| <= 2^53) and would lose precision", val.GetInt64Val())
}
}
for i, val := range values {
if IsInteger(val) {
values[i] = NewFloat(float64(val.GetInt64Val()))
@@ -137,6 +137,41 @@ func TestExpr_Term(t *testing.T) {
}
}
// TestExpr_Term_JSONMixedNumeric covers issue #48442: mixing int64 and float
// literals in a JSON 'in' list used to coerce every int64 to float64, causing
// distinct large integers to collapse to the same IEEE-754 double and produce
// incorrect matches. The parser now rejects the query when any integer would
// lose precision under float64.
func TestExpr_Term_JSONMixedNumeric(t *testing.T) {
schema := newTestSchema(true)
helper, err := typeutil.CreateSchemaHelper(schema)
assert.NoError(t, err)
// Safe cases: ints fit in float64 exactly (|v| <= 2^53) — coercion keeps
// full precision, so the parser still accepts the query.
validExprs := []string{
`JSONField["A"] in [1, 1.5]`,
`JSONField["A"] in [9007199254740992, 1.5]`, // exactly 2^53
`JSONField["A"] in [-9007199254740992, 1.5]`, // exactly -2^53
`JSONField["A"] not in [2, 3.14]`,
}
for _, exprStr := range validExprs {
assertValidExpr(t, helper, exprStr)
}
// Unsafe cases: an integer outside the safe range is mixed with a float.
// Coercion would lose precision, so the parser must reject the query.
invalidExprs := []string{
`JSONField["A"] in [9223372036854775807, 1.5]`,
`JSONField["A"] in [9007199254740993, 1.5]`, // 2^53 + 1
`JSONField["A"] in [1.5, -9007199254740993]`, // -(2^53 + 1)
`JSONField["A"] not in [9223372036854775800, 0.0]`,
}
for _, exprStr := range invalidExprs {
assertInvalidExpr(t, helper, exprStr)
}
}
func TestExpr_Call(t *testing.T) {
schema := newTestSchema(true)
helper, err := typeutil.CreateSchemaHelper(schema)
+9
View File
@@ -70,6 +70,15 @@ func IsNumber(n *planpb.GenericValue) bool {
return IsInteger(n) || IsFloating(n)
}
// fitsInFloat64Exactly reports whether v can be round-tripped through float64
// without precision loss. Any int64 with |v| > 2^53 collapses with its
// neighbours under IEEE-754 double, so distinct values become indistinguishable
// after coercion.
func fitsInFloat64Exactly(v int64) bool {
const maxSafe = int64(1) << 53 // 9007199254740992
return v >= -maxSafe && v <= maxSafe
}
func IsString(n *planpb.GenericValue) bool {
switch n.GetVal().(type) {
case *planpb.GenericValue_StringVal: