chris
chris

Reputation: 4351

sqlx postgres scan method fail

I am trying to perform a postgres query that contains a custom geoPoint type but get a Unexpected EOF error. Any ideas to what I am doing wrong?

type Account struct {
    Id             uint   `json:"id" db:"id"`
    RegistrationId string `json:"registration_id" db:"registration_id"`
    PhoneNumber    string `json:"phone_number" db:"phone_number"`
    AuthToken      string `json:"auth_token" db:"auth_token"`
    // Role           string `json:"-" db:"role"`
    CreatedAt   time.Time `json:"-" db:"created_at"`
    ActivatedAt time.Time `json:"-" db:"activated_at"`

    Location GeoPoint `json:"location" db:"location"`
}

// THE FAILING FUNCTION
func FindAccountByToken(db *sqlx.DB, token string) (Account, error) {
    var account Account
    log.Println("FindAcountByToken", token)
    err := db.Get(&account, "select * from accounts where auth_token = $1", token)
    return account, err
}

type GeoPoint struct {
    Latitude  float64 `json:"latitude" db:"latitude"`
    Longitude float64 `json:"longitude" db:"longitude"`
}

// String value
func (g *GeoPoint) String() string {
    return fmt.Sprintf("(%v, %v)", g.Latitude, g.Longitude)
}

// Value of the geoPoint to be stored in the db based on the .String() method
func (g GeoPoint) Value() (driver.Value, error) {
    return g.String(), nil
}

// Scan converts the db []byte array value to the geoPoint value
func (g *GeoPoint) Scan(src interface{}) error {
    var source []byte
    var gp GeoPoint

    switch src.(type) {
    case []byte:
        source = src.([]byte)
    default:
        return errors.New("Unable to perform geopoint conversion")
    }

    log.Println("bytes -> ", source)

    reader := bytes.NewReader(source)
    if err := binary.Read(reader, binary.BigEndian, &gp); err != nil {
        log.Println("BinaryRead Error", err)
        return err
    }

    *g = gp

    return nil
}

Upvotes: 1

Views: 643

Answers (1)

dyoo
dyoo

Reputation: 12003

The GeoPoint implementation of Scanner and Valuer interfaces looks questionable. They should be symmetric, but in one case, it represents the GeoPoint as a arbitrary-variable-length string of the form:

"(<latitude>, <longitude>)"

but in the other direction, expects a representation of exactly 16 bytes (two 64-bit floats in big-endian byte order). This does not seem compatible.

Have you tested that the output value of Value() can be fed back into Scan() to get the same value? Essentially, you should be able to do:

p1 := GeoPoint{3.1415926, 2.71828}
bs, _ := p1.Value()
p2 := GeoPoint{}
p2.Scan(bs)
fmt.Printf("%#v\n", p2) 

and see the same value in p2 as in p1.

For example, something like:

// Value of the geoPoint to be stored in the db
func (g *GeoPoint) Value() (driver.Value, error) {
    var buf bytes.Buffer
    binary.Write(&buf, binary.BigEndian, g)
    return buf.Bytes(), nil
}

// Scan converts the db []byte array value to the geoPoint value
func (g *GeoPoint) Scan(src interface{}) error {
    var source []byte
    switch src.(type) {
    case []byte:
        source = src.([]byte)
    default:
        return errors.New("Unable to perform geopoint conversion")
    }
    reader := bytes.NewReader(source)
    return binary.Read(reader, binary.BigEndian, g)
}

should satisfy this property. Scan() and Value() have to be conceptually consistent: otherwise, they wouldn't be useful.

Upvotes: 1

Related Questions