Skip to content

Fix spurious validate= due to float arithmetics bug#462

Open
booxter wants to merge 2 commits intoovn-kubernetes:mainfrom
booxter:modelgen-diff-not-detected-in-ci
Open

Fix spurious validate= due to float arithmetics bug#462
booxter wants to merge 2 commits intoovn-kubernetes:mainfrom
booxter:modelgen-diff-not-detected-in-ci

Conversation

@booxter
Copy link

@booxter booxter commented Feb 6, 2026

math.Pow returns float64, which - at this power level (^63) - is
imprecise. It means that when we compared:

int(math.Pow(2, 63) - 1 and math.MaxInt64

in getIntegerValidations, we always got inequality and a spurious
validate= tag in the generated models.

This results in the following diff if make lint is executed:

--- a/ovsdb/serverdb/database.go
+++ b/ovsdb/serverdb/database.go
@@ -22,11 +22,11 @@ type Database struct {
 	UUID      string        `ovsdb:"_uuid"`
 	Cid       *string       `ovsdb:"cid"`
 	Connected bool          `ovsdb:"connected"`
-	Index     *int          `ovsdb:"index"`
+	Index     *int          `ovsdb:"index" validate:"omitempty,max=9223372036854775806"`
 	Leader    bool          `ovsdb:"leader"`
-	Model     DatabaseModel `ovsdb:"model" validate:"oneof='standalone' 'clustered' 'relay'"`
-	Name      string        `ovsdb:"name"`
-	Schema    *string       `ovsdb:"schema"`
+	Model     DatabaseModel `ovsdb:"model" validate:"max=9223372036854775806,oneof='standalone' 'clustered' 'relay'"`
+	Name      string        `ovsdb:"name" validate:"max=9223372036854775806"`
+	Schema    *string       `ovsdb:"schema" validate:"omitempty,max=9223372036854775806"`
 	Sid       *string       `ovsdb:"sid"`
 }

Good news is there's no requirement to use math.Pow here, since we can
just refer to math.MaxInt64, so we can replace the formula with it.

I believe the original math.Pow implementation was supposed to reflect
the mathematical formula from RFC7047:

   <integer>
      A JSON number with an integer value, within the range -(2**63)...+
      (2**63)-1.

...but it didn't consider float arithmetics implications.

math.Pow returns float64, which - at this power level (^63) - is
imprecise. It means that when we compared:

`int(math.Pow(2, 63) - 1` and `math.MaxInt64`

in `getIntegerValidations`, we always got inequality and a spurious
validate= tag in the generated models.

This results in the following diff if `make lint` is executed:

```diff
--- a/ovsdb/serverdb/database.go
+++ b/ovsdb/serverdb/database.go
@@ -22,11 +22,11 @@ type Database struct {
 	UUID      string        `ovsdb:"_uuid"`
 	Cid       *string       `ovsdb:"cid"`
 	Connected bool          `ovsdb:"connected"`
-	Index     *int          `ovsdb:"index"`
+	Index     *int          `ovsdb:"index" validate:"omitempty,max=9223372036854775806"`
 	Leader    bool          `ovsdb:"leader"`
-	Model     DatabaseModel `ovsdb:"model" validate:"oneof='standalone' 'clustered' 'relay'"`
-	Name      string        `ovsdb:"name"`
-	Schema    *string       `ovsdb:"schema"`
+	Model     DatabaseModel `ovsdb:"model" validate:"max=9223372036854775806,oneof='standalone' 'clustered' 'relay'"`
+	Name      string        `ovsdb:"name" validate:"max=9223372036854775806"`
+	Schema    *string       `ovsdb:"schema" validate:"omitempty,max=9223372036854775806"`
 	Sid       *string       `ovsdb:"sid"`
 }
```

Good news is there's no requirement to use `math.Pow` here, since we can
just refer to `math.MaxInt64`, so we can replace the formula with it.

I believe the original `math.Pow` implementation was supposed to reflect
the mathematical formula from RFC7047:

```
   <integer>
      A JSON number with an integer value, within the range -(2**63)...+
      (2**63)-1.
```

...but it didn't consider float arithmetics implications.

Signed-off-by: Ihar Hrachyshka <[email protected]>
@booxter
Copy link
Author

booxter commented Feb 6, 2026

package main

import (
	"fmt"
	"math"
)

func main() {
	powValue := int(math.Pow(2, 63)) - 1
	maxInt64 := math.MaxInt64

	// Calculate 2^63 by multiplying 2 sixty-three times
	multiplied := 1
	for i := 0; i < 63; i++ {
		multiplied *= 2
	}
	multiplied -= 1 // subtract 1 to match the others

	fmt.Printf("int(math.Pow(2, 63)) - 1 = %d\n", powValue)
	fmt.Printf("math.MaxInt64            = %d\n", maxInt64)
	fmt.Printf("2*2*...(63 times) - 1    = %d\n", multiplied)
	fmt.Println()
	fmt.Printf("Pow == MaxInt64?         = %v\n", powValue == maxInt64)
	fmt.Printf("Multiplied == MaxInt64?  = %v\n", multiplied == maxInt64)
}
$ go run ...
int(math.Pow(2, 63)) - 1 = 9223372036854775806
math.MaxInt64            = 9223372036854775807
2*2*...(63 times) - 1    = 9223372036854775807

Pow == MaxInt64?         = false
Multiplied == MaxInt64?  = true

The exact power level at which this breaks down can be demonstrated with:

package main

import (
	"fmt"
	"math"
)

func main() {
	for n := 50; n <= 55; n++ {
		powFloat := math.Pow(2, float64(n))
		canDistinguish := (powFloat + 1) != powFloat
		fmt.Printf("2^%d + 1 != 2^%d: %v\n", n, n, canDistinguish)
	}
}
$ go run ...
2^50 + 1 != 2^50: true
2^51 + 1 != 2^51: true
2^52 + 1 != 2^52: true
2^53 + 1 != 2^53: false
2^54 + 1 != 2^54: false
2^55 + 1 != 2^55: false

@booxter booxter marked this pull request as ready for review February 6, 2026 01:51
@booxter
Copy link
Author

booxter commented Feb 6, 2026

One thing I note in the spurious diff without the fix is that some fields get omitempty. I wonder if the validator should actually attach these attributes to the fields, and doesn't because of another bug in the generator. (e.g. not adding any legit attributes if max= validation is omitted?) Someone with more knowledge as to the intent here could probably be of help.

Nevermind, omitempty probably refers to omitting validations, so if there are none, then there's nothing to omit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant