SQL Expressions: Replace NaN/Inf values with Null (#112641)

This is because MySQL doesn't support storing of NaN valuels, and therefore go-mysql-server isn't going to either.

Float fields/columns are always mapped to be nullable now, otherwise we would have to replace NaN/Inf with 0.
This commit is contained in:
Kyle Brandt
2025-10-21 10:43:04 -04:00
committed by GitHub
parent 5d8492d728
commit e930b3e6c4
4 changed files with 100 additions and 7 deletions
+84 -3
View File
@@ -5,6 +5,7 @@ package sql
import (
"context"
"encoding/json"
"math"
"net/http"
"testing"
"time"
@@ -123,13 +124,62 @@ func TestQueryFramesInOut(t *testing.T) {
},
}
// Same as input but float32/64 fields always become nullable so NaN/Inf can be mapped to null
expected := &data.Frame{
RefID: "b",
Name: "b",
Fields: []*data.Field{
data.NewField("time", nil, []time.Time{time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC), time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)}),
data.NewField("time_nullable", nil, []*time.Time{p(time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)), nil}),
data.NewField("string", nil, []string{"cat", "dog"}),
data.NewField("null_nullable", nil, []*string{p("cat"), nil}),
data.NewField("bool", nil, []bool{true, false}),
data.NewField("bool_nullable", nil, []*bool{p(true), nil}),
// Floats
data.NewField("float32", nil, []*float32{p(float32(1)), p(float32(3))}),
data.NewField("float32_nullable", nil, []*float32{p(float32(2.0)), nil}),
data.NewField("float64", nil, []*float64{p(float64(1)), p(float64(3))}),
data.NewField("float64_nullable", nil, []*float64{p(float64(2.0)), nil}),
// Ints
data.NewField("int8", nil, []int8{1, 3}),
data.NewField("int8_nullable", nil, []*int8{p(int8(2)), nil}),
data.NewField("int16", nil, []int16{1, 3}),
data.NewField("int16_nullable", nil, []*int16{p(int16(2)), nil}),
data.NewField("int32", nil, []int32{1, 3}),
data.NewField("int32_nullable", nil, []*int32{p(int32(2)), nil}),
data.NewField("int64", nil, []int64{1, 3}),
data.NewField("int64_nullable", nil, []*int64{p(int64(2)), nil}),
// Unsigned Ints
data.NewField("uint8", nil, []uint8{1, 3}),
data.NewField("uint8_nullable", nil, []*uint8{p(uint8(2)), nil}),
data.NewField("uint16", nil, []uint16{1, 3}),
data.NewField("uint16_nullable", nil, []*uint16{p(uint16(2)), nil}),
data.NewField("uint32", nil, []uint32{1, 3}),
data.NewField("uint32_nullable", nil, []*uint32{p(uint32(2)), nil}),
data.NewField("uint64", nil, []uint64{1, 3}),
data.NewField("uint64_nullable", nil, []*uint64{p(uint64(2)), nil}),
},
}
db := DB{}
qry := `SELECT * from a`
resultFrame, err := db.QueryFrames(context.Background(), &testTracer{}, "a", qry, []*data.Frame{frameA})
resultFrame, err := db.QueryFrames(context.Background(), &testTracer{}, "b", qry, []*data.Frame{frameA})
require.NoError(t, err)
if diff := cmp.Diff(frameA, resultFrame, data.FrameTestCompareOptions()...); diff != "" {
if diff := cmp.Diff(expected, resultFrame, data.FrameTestCompareOptions()...); diff != "" {
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
}
}
@@ -139,7 +189,7 @@ func TestQueryFramesNumericSelect(t *testing.T) {
RefID: "a",
Name: "a",
Fields: []*data.Field{
data.NewField("decimal", nil, []float64{2.35}),
data.NewField("decimal", nil, []*float64{p(2.35)}),
data.NewField("tinySigned", nil, []int8{-128}),
data.NewField("smallSigned", nil, []int16{-32768}),
data.NewField("mediumSigned", nil, []int32{-8388608}),
@@ -197,6 +247,37 @@ func TestQueryFramesDateTimeSelect(t *testing.T) {
}
}
func TestNaNBecomesNull(t *testing.T) {
f := &data.Frame{
RefID: "a",
Name: "a",
Fields: []*data.Field{
data.NewField("d", nil, []float64{2.35, math.NaN()}),
data.NewField("e", nil, []*float64{p(3.1), p(math.NaN())}),
},
}
expectedFrame := &data.Frame{
RefID: "b",
Name: "b",
Fields: []*data.Field{
data.NewField("d", nil, []*float64{p(2.35), nil}),
data.NewField("e", nil, []*float64{p(3.1), nil}),
},
}
db := DB{}
qry := `SELECT * FROM a`
ret, err := db.QueryFrames(context.Background(), &testTracer{}, "b", qry, []*data.Frame{f})
if diff := cmp.Diff(expectedFrame, ret, data.FrameTestCompareOptions()...); diff != "" {
require.FailNowf(t, "Result mismatch (-want +got):%s\n", diff)
}
require.NoError(t, err)
}
func TestErrorsFromGoMySQLServerAreFlagged(t *testing.T) {
const GmsNotImplemented = "TRUNCATE" // not implemented in go-mysql-server as of 2025-04-11