-
-
Notifications
You must be signed in to change notification settings - Fork 1k
How to handle JSONB with int64 within a map? #289
Description
Hi,
First, thanks for this awesome library - we are using it in production and it works great! I really appreciate you making this available. :-)
I'm looking for guidance on how to handle int64 values in a jsonb Postgres column that stores a map. The problem is, values are marshaled by go as int64 when written to Postgres but are unmarshaled back as float64. As long as the original int64 can be encoded in 53 bits or fewer, there's no loss of precision. However, for values larger than 2^53, especially 64-bit ids, the loss of precision produces wrong results - go rounds off the int64 when it converts it back to a float 64 during unmarshaling from Postgres.
Example:
// Table is
// id: bigserial
// some_data: jsonb
// The field some_data may hold a Person or a Pet ...
type Person struct {
PersonId int64
Name string
}
type Pet struct {
PetId int64
Species string
}
p1 := Person{PersonId: 1499366092322157987, Name: "Joel"}
// p1 Encodes to JSONB as
//
// '{"PersonId": 1499366092322157987, "Name": "Joel"}'
// Now read the row just written with p1 back into p2
var someData interface{}
err := p.Pool.QueryRow("select some_data from some_table limit1").Scan(&someData)
// Now map someData back to a Person or a Pet as appropriate.
// The issue is that someData will be a map[string]interface{}
// and both someData["PersonId"] and someData["PetId"] will be float64 not int64.
// This is the flaw in go's json unmarshaling, decoding an integer into a float.
// p2 converted from someData will be:
// PersonId: 1499366092322158080
// Name: "Joel"
// Notice how the PersonId got rounded off by go's json unmarshaling and the meaningful value
// of the PersonId is lost - the original PersonId is 1499366092322157987 not the go-rounded value
// 1499366092322158080. This PersonId is 60 bits, so it gets rounded because it can't fit in the mantissa of a float64 which reserves only 53 bits for the mantissa.
One way around this is:
var someData2 []byte
err = p.Pool.QueryRow("select some_data from some_table limit1").Scan(&someData2)
// [1:] is to skip the '\x01' byte at index zero which is returned by Postgres?
decoder := json.NewDecoder(bytes.NewReader(someData2[1:]))
decoder.UseNumber()
var p2v2 interface{}
decoder.Decode(&p2v2)
// Now p2v2["PersonId"] is the original, correct int64 value 1499366092322157987
// but stored as a json.Number. However, when mapped back to a Person struct
// it comes back as an int64.
However, this feels fragile since I'm relying on byte 0 being the sentinel \x01 which could change I guess?
Any other ideas?
Note that the some_data field can stored different types of structs, so I can't unmarshal directly back to a struct. The table design can't be changed at this point.
Is there a better way to handle this?