Skip to content

Commit b8d1c9c

Browse files
committed
sql/parser: avoid float -> unsigned conversions
While investigating this, I discovered that our current `round` implementation is quite dubious, and does not rely on any referenced source material or any material that I could find. I also found that Postgres does not implement 2-ary `round` where the first argument is a float. The above is addressed by: - replacing `round(float)` with a transcription of Postgres' `rint`. - replacing `round(float, int)` with an implementation that round-trips through apd. This is likely much slower, but likely correct. Updates #14405.
1 parent 6dda974 commit b8d1c9c

File tree

4 files changed

+151
-32
lines changed

4 files changed

+151
-32
lines changed

pkg/cmd/metacheck/main.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package main
1818

1919
import (
20+
"go/ast"
21+
"go/types"
2022
"log"
2123
"os"
2224

@@ -38,7 +40,9 @@ func (m *metaChecker) Init(program *lint.Program) {
3840
}
3941

4042
func (m *metaChecker) Funcs() map[string]lint.Func {
41-
funcs := make(map[string]lint.Func)
43+
funcs := map[string]lint.Func{
44+
"FloatToUnsigned": checkConvertFloatToUnsigned,
45+
}
4246
for _, checker := range m.checkers {
4347
for k, v := range checker.Funcs() {
4448
if _, ok := funcs[k]; ok {
@@ -51,6 +55,57 @@ func (m *metaChecker) Funcs() map[string]lint.Func {
5155
return funcs
5256
}
5357

58+
// @ianlancetaylor via golang-nuts[0]:
59+
//
60+
// For the record, the spec says, in https://golang.org/ref/spec#Conversions:
61+
// "In all non-constant conversions involving floating-point or complex
62+
// values, if the result type cannot represent the value the conversion
63+
// succeeds but the result value is implementation-dependent." That is the
64+
// case that applies here: you are converting a negative floating point number
65+
// to uint64, which can not represent a negative value, so the result is
66+
// implementation-dependent. The conversion to int64 works, of course. And
67+
// the conversion to int64 and then to uint64 succeeds in converting to int64,
68+
// and when converting to uint64 follows a different rule: "When converting
69+
// between integer types, if the value is a signed integer, it is sign
70+
// extended to implicit infinite precision; otherwise it is zero extended. It
71+
// is then truncated to fit in the result type's size."
72+
//
73+
// So, basically, don't convert a negative floating point number to an
74+
// unsigned integer type.
75+
//
76+
// [0] https://groups.google.com/d/msg/golang-nuts/LH2AO1GAIZE/PyygYRwLAwAJ
77+
//
78+
// TODO(tamird): upstream this.
79+
func checkConvertFloatToUnsigned(j *lint.Job) {
80+
fn := func(node ast.Node) bool {
81+
call, ok := node.(*ast.CallExpr)
82+
if !ok {
83+
return true
84+
}
85+
castType, ok := j.Program.Info.TypeOf(call.Fun).(*types.Basic)
86+
if !ok {
87+
return true
88+
}
89+
if castType.Info()&types.IsUnsigned == 0 {
90+
return true
91+
}
92+
for _, arg := range call.Args {
93+
argType, ok := j.Program.Info.TypeOf(arg).(*types.Basic)
94+
if !ok {
95+
continue
96+
}
97+
if argType.Info()&types.IsFloat == 0 {
98+
continue
99+
}
100+
j.Errorf(arg, "do not convert a floating point number to an unsigned integer type")
101+
}
102+
return true
103+
}
104+
for _, f := range j.Program.Files {
105+
ast.Inspect(f, fn)
106+
}
107+
}
108+
54109
func main() {
55110
unusedChecker := unused.NewChecker(unused.CheckAll)
56111
unusedChecker.WholeProgram = true

pkg/sql/parser/builtins.go

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,7 +1263,7 @@ var Builtins = map[string][]Builtin{
12631263

12641264
"round": {
12651265
floatBuiltin1(func(x float64) (Datum, error) {
1266-
return round(x, 0)
1266+
return NewDFloat(DFloat(round(x))), nil
12671267
}, "Rounds `val` to the nearest integer using half to even (banker's) rounding."),
12681268
decimalBuiltin1(func(x *apd.Decimal) (Datum, error) {
12691269
return roundDecimal(x, 0)
@@ -1273,7 +1273,25 @@ var Builtins = map[string][]Builtin{
12731273
Types: ArgTypes{{"input", TypeFloat}, {"decimal_accuracy", TypeInt}},
12741274
ReturnType: fixedReturnType(TypeFloat),
12751275
fn: func(_ *EvalContext, args Datums) (Datum, error) {
1276-
return round(float64(*args[0].(*DFloat)), int64(MustBeDInt(args[1])))
1276+
var x apd.Decimal
1277+
if _, err := x.SetFloat64(float64(*args[0].(*DFloat))); err != nil {
1278+
return nil, err
1279+
}
1280+
1281+
// TODO(mjibson): make sure this fits in an int32.
1282+
scale := int32(MustBeDInt(args[1]))
1283+
1284+
var d apd.Decimal
1285+
if _, err := RoundCtx.Quantize(&d, &x, -scale); err != nil {
1286+
return nil, err
1287+
}
1288+
1289+
f, err := d.Float64()
1290+
if err != nil {
1291+
return nil, err
1292+
}
1293+
1294+
return NewDFloat(DFloat(f)), nil
12771295
},
12781296
Info: "Keeps `decimal_accuracy` number of figures to the right of the zero position " +
12791297
" in `input` using half to even (banker's) rounding.",
@@ -2082,36 +2100,72 @@ func overlay(s, to string, pos, size int) (Datum, error) {
20822100
return NewDString(string(runes[:pos]) + to + string(runes[after:])), nil
20832101
}
20842102

2085-
func round(x float64, n int64) (Datum, error) {
2086-
pow := math.Pow(10, float64(n))
2103+
// Transcribed from Postgres' src/port/rint.c, with c-style comments preserved
2104+
// for ease of mapping.
2105+
//
2106+
// https://github.com/postgres/postgres/blob/REL9_6_3/src/port/rint.c
2107+
func round(x float64) float64 {
2108+
/* Per POSIX, NaNs must be returned unchanged. */
2109+
if math.IsNaN(x) {
2110+
return x
2111+
}
20872112

2088-
if pow == 0 {
2089-
// Rounding to so many digits on the left that we're underflowing.
2090-
// Avoid a NaN below.
2091-
return NewDFloat(DFloat(0)), nil
2113+
/* Both positive and negative zero should be returned unchanged. */
2114+
if x == 0.0 {
2115+
return x
20922116
}
2093-
if math.Abs(x*pow) > 1e17 {
2094-
// Rounding touches decimals below float precision; the operation
2095-
// is a no-op.
2096-
return NewDFloat(DFloat(x)), nil
2117+
2118+
roundFn := math.Ceil
2119+
if math.Signbit(x) {
2120+
roundFn = math.Floor
20972121
}
20982122

2099-
v, frac := math.Modf(x * pow)
2100-
// The following computation implements unbiased rounding, also
2101-
// called bankers' rounding. It ensures that values that fall
2102-
// exactly between two integers get equal chance to be rounded up or
2103-
// down.
2104-
if x > 0.0 {
2105-
if frac > 0.5 || (frac == 0.5 && uint64(v)%2 != 0) {
2106-
v += 1.0
2107-
}
2108-
} else {
2109-
if frac < -0.5 || (frac == -0.5 && uint64(v)%2 != 0) {
2110-
v -= 1.0
2111-
}
2123+
/*
2124+
* Subtracting 0.5 from a number very close to -0.5 can round to
2125+
* exactly -1.0, producing incorrect results, so we take the opposite
2126+
* approach: add 0.5 to the negative number, so that it goes closer to
2127+
* zero (or at most to +0.5, which is dealt with next), avoiding the
2128+
* precision issue.
2129+
*/
2130+
xOrig := x
2131+
x -= math.Copysign(0.5, x)
2132+
2133+
/*
2134+
* Be careful to return minus zero when input+0.5 >= 0, as that's what
2135+
* rint() should return with negative input.
2136+
*/
2137+
if x == 0 || math.Signbit(x) != math.Signbit(xOrig) {
2138+
return math.Copysign(0.0, xOrig)
2139+
}
2140+
2141+
/*
2142+
* For very big numbers the input may have no decimals. That case is
2143+
* detected by testing x+0.5 == x+1.0; if that happens, the input is
2144+
* returned unchanged. This also covers the case of minus infinity.
2145+
*/
2146+
if x == xOrig-math.Copysign(1.0, x) {
2147+
return xOrig
2148+
}
2149+
2150+
/* Otherwise produce a rounded estimate. */
2151+
r := roundFn(x)
2152+
2153+
/*
2154+
* If the rounding did not produce exactly input+0.5 then we're done.
2155+
*/
2156+
if r != x {
2157+
return r
21122158
}
21132159

2114-
return NewDFloat(DFloat(v / pow)), nil
2160+
/*
2161+
* The original fractional part was exactly 0.5 (since
2162+
* floor(input+0.5) == input+0.5). We need to round to nearest even.
2163+
* Dividing input+0.5 by 2, taking the floor and multiplying by 2
2164+
* yields the closest even number. This part assumes that division by
2165+
* 2 is exact, which should be OK because underflow is impossible
2166+
* here: x is an integer.
2167+
*/
2168+
return roundFn(x*0.5) * 2.0
21152169
}
21162170

21172171
func roundDecimal(x *apd.Decimal, n int32) (Datum, error) {

pkg/sql/parser/decimal.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,11 @@ var (
3232
ExactCtx = DecimalCtx.WithPrecision(0)
3333
// HighPrecisionCtx is a decimal context with high precision.
3434
HighPrecisionCtx = DecimalCtx.WithPrecision(2000)
35+
// RoundCtx is a decimal context with high precision and RoundHalfEven
36+
// rounding.
37+
RoundCtx = func() *apd.Context {
38+
ctx := *HighPrecisionCtx
39+
ctx.Rounding = apd.RoundHalfEven
40+
return &ctx
41+
}()
3542
)

pkg/sql/testdata/logic_test/builtin_function

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,9 @@ SELECT radians(-45.0), radians(45.0)
655655
----
656656
-0.7853981633974483 0.7853981633974483
657657

658+
query error invalid operation
659+
SELECT round(123.456::float, -2438602134409251682)
660+
658661
query RRR
659662
SELECT round(4.2::float, 0), round(4.2::float, 10), round(4.22222222::decimal, 3)
660663
----
@@ -710,12 +713,12 @@ SELECT round(-1.7976931348623157e+308::float, 1), round(1.7976931348623157e+308:
710713
query RR
711714
SELECT round(-1.7976931348623157e+308::float, -303), round(1.7976931348623157e+308::float, -303)
712715
----
713-
-1.797690000000001e+308 1.797690000000001e+308
716+
-1.79769e+308 1.79769e+308
714717

715718
query RR
716719
SELECT round(-1.23456789e+308::float, -308), round(1.23456789e+308::float, -308)
717720
----
718-
-1.0000000000000006e+308 1.0000000000000006e+308
721+
-1e+308 1e+308
719722

720723
query RR
721724
SELECT round(-1.7976931348623157e-308::float, 1), round(1.7976931348623157e-308::float, 1)
@@ -727,10 +730,10 @@ SELECT 1.234567890123456789::float, round(1.234567890123456789::float,15), roun
727730
----
728731
1.2345678901234567 1.234567890123457 1.2345678901234567 1.2345678901234567
729732

730-
query RRRR
731-
SELECT round(123.456::float, -1), round(123.456::float, -2), round(123.456::float, -3), round(123.456::float, -2438602134409251682)
733+
query RRR
734+
SELECT round(123.456::float, -1), round(123.456::float, -2), round(123.456::float, -3)
732735
----
733-
120 100 0 0
736+
120 100 0
734737

735738
query RRRR
736739
SELECT round(123.456::decimal, -1), round(123.456::decimal, -2), round(123.456::decimal, -3), round(123.456::decimal, -200)

0 commit comments

Comments
 (0)