4

I referenced irbanana's answer about supporting Spatial data type for PostGIS. I'm using MySQL and am trying to implement Value() for the custom data type EWKBGeomPoint.

My Gorm model:

import (
    "github.com/twpayne/go-geom"
    "github.com/twpayne/go-geom/encoding/ewkb"
)

type EWKBGeomPoint geom.Point

type Tag struct {
    Name string `json:"name"`
json:"siteID"` // forign key
    Loc EWKBGeomPoint `json:"loc"`
}

From what I know, MySQL supports insertion like this:

INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name',ST_GeomFromText('POINT(10.000000 20.000000)'))

or

INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name', ST_GeomFromWKB(X'0101000000000000000000F03F000000000000F03F'))

If I do a my own Value() to satisfy the database/sql's Valuer interface:

func (g EWKBGeomPoint) Value() (driver.Value, error) {
    log.Println("EWKBGeomPoint value called")
    b := geom.Point(g)
    bp := &b

    floatArr := bp.Coords()
    return fmt.Sprintf("ST_GeomFromText('POINT(%f %f)')", floatArr[0], floatArr[1]), nil
}

The entire value including ST_GeomFromText() is quoted in a single quote from Gorm, and so it won't work:

INSERT INTO `tag` (`name`,`loc`) VALUES ('tag name','ST_GeomFromText('POINT(10.000000 20.000000)')');

How do I make it work?

EDIT 1:

I trace into Gorm code, eventually it get's to callback_create.go's createCallback function. Inside it check for if primaryField == nil and it is true, it goes into calling scope.SQLDB().Exec then I failed to trace further.

scope.SQL is string INSERT INTOtag(name,loc) VALUES (?,?) and scope.SQLVars prints [tag name {{1 2 [10 20] 0}}]. It looks like interpolation happens inside this call.

Is this calling into database/sql code?

EDIT 2:

Found a similar Stackoverflow question here. But I do not understand the solution.

gstvg
  • 889
  • 4
  • 10
huggie
  • 17,587
  • 27
  • 82
  • 139
  • Could you use [raw sql](http://jinzhu.me/gorm/advanced.html#sql-builder)? – Mark Mar 04 '20 at 09:46
  • @Mark I could but I definitely do not want to. I have a REST service with CRUD already defined. If this works the way I think it should, once I defined the schema, all the endpoint handlers do not need to be duplicated for each table, and marshalling/unmarshalling to and from JSON are just a little work. – huggie Mar 04 '20 at 10:11
  • 1
    Why doesn't the raw `SELECT LAST_INSERT_ID()` satisfy the requirements for using auto_inc with raw queries? It is tied to the connection, so there is no interference from other connections. – Rick James Mar 06 '20 at 16:53
  • Does GORM have no way to "escape" quotes so that you can have nested quotes? Anyway, in most, if not all, MySQL syntax situations, single-quotes and double-quotes are interchangeable. This gives 2 layers without needing any escaping. – Rick James Mar 06 '20 at 16:55
  • @RickJames I don't think nested quote works. The db function shouldn't be quoted at all. And you're right about `last_insert_id()` I am not well versed with SQL. Thank you for your tip. :) – huggie Mar 09 '20 at 04:03

2 Answers2

3

Here's another approach; use binary encoding.

According to this doc, MySQL stores geometry values using 4 bytes to indicate the SRID (Spatial Reference ID) followed by the WKB (Well Known Binary) representation of the value.

So a type can use WKB encoding and add and remove the four byte prefix in Value() and Scan() functions. The go-geom library found in other answers has a WKB encoding package, github.com/twpayne/go-geom/encoding/wkb.

For example:

type MyPoint struct {
    Point wkb.Point
}

func (m *MyPoint) Value() (driver.Value, error) {
    value, err := m.Point.Value()
    if err != nil {
        return nil, err
    }

    buf, ok := value.([]byte)
    if !ok {
        return nil, fmt.Errorf("did not convert value: expected []byte, but was %T", value)
    }

    mysqlEncoding := make([]byte, 4)
    binary.LittleEndian.PutUint32(mysqlEncoding, 4326)
    mysqlEncoding = append(mysqlEncoding, buf...)

    return mysqlEncoding, err
}

func (m *MyPoint) Scan(src interface{}) error {
    if src == nil {
        return nil
    }

    mysqlEncoding, ok := src.([]byte)
    if !ok {
        return fmt.Errorf("did not scan: expected []byte but was %T", src)
    }

    var srid uint32 = binary.LittleEndian.Uint32(mysqlEncoding[0:4])

    err := m.Point.Scan(mysqlEncoding[4:])

    m.Point.SetSRID(int(srid))

    return err
}

Defining a Tag using the MyPoint type:

type Tag struct {
    Name string   `gorm:"type:varchar(50);primary_key"`
    Loc  *MyPoint `gorm:"column:loc"`
}

func (t Tag) String() string {
    return fmt.Sprintf("%s @ Point(%f, %f)", t.Name, t.Loc.Point.Coords().X(), t.Loc.Point.Coords().Y())
}

Creating a tag using the type:

tag := &Tag{
    Name: "London",
    Loc: &MyPoint{
        wkb.Point{
            geom.NewPoint(geom.XY).MustSetCoords([]float64{0.1275, 51.50722}).SetSRID(4326),
        },
    },
}

err = db.Create(&tag).Error
if err != nil {
    log.Fatalf("create: %v", err)
}

MySQL results:

mysql> describe tag;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| name  | varchar(50) | NO   | PRI | NULL    |       |
| loc   | geometry    | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+


mysql> select name, st_astext(loc) from tag;
+--------+------------------------+
| name   | st_astext(loc)         |
+--------+------------------------+
| London | POINT(0.1275 51.50722) |
+--------+------------------------+
  • (ArcGIS says 4326 is the most common spatial reference for storing a referencing data across the entire world. It serves as the default for both the PostGIS spatial database and the GeoJSON standard. It is also used by default in most web mapping libraries.)
Mark
  • 6,731
  • 1
  • 40
  • 38
  • Thank you so much for the detailed explanation. It works out beautifully. Without your help I could not have figured it out. There is also an `encoding/wekb` but the value came out really close to 0 and I do not know why. Also I tried to use type cast instead of encapsulation, but the length of the buffer (src) coming from the scan interface seems wrong. But with your approach it works. – huggie Mar 09 '20 at 03:59
  • No worries, hope it helps. – Mark Mar 10 '20 at 08:55
0

Update: this approach didn't work.

Hooks may let you set the column to a gorm.Expr before Gorm's sql generation.

For example, something like this before insert:

func (t *Tag) BeforeCreate(scope *gorm.Scope) error {

  x, y := .... // tag.Loc coordinates

  text := fmt.Sprintf("POINT(%f %f)", x, y)

  expr := gorm.Expr("ST_GeomFromText(?)", text)

  scope.SetColumn("loc", expr)

  return nil
}
Mark
  • 6,731
  • 1
  • 40
  • 38
  • Curious enough, in this case my custom scanner is called with argument *gorm.SqlExpr. And I don't know what to do with it. This scanner is needed during the query. Commenting it out also didn't help with creating data. I received `Error 1364: Field 'loc' doesn't have a default value`. – huggie Mar 05 '20 at 07:25
  • I added my trace result before adding your hook. – huggie Mar 05 '20 at 07:30
  • 1
    `field 'loc' doesn't have a default value` occurs if column is not null in db, but it isn't set for insert (eg, ignored/not exported), and Gorm seems to require a default value on the model as an attribute. Scan is called when preparing the value for insert, I don't know why but assume this is to double check. And although you can call SetColumn to set an *SqlExpr, it's not used when inserting. I think it would be used if the original type of the field was *SqlExpr. So sorry, it seems this approach won't work. Another idea might be to create your own dialect. – Mark Mar 05 '20 at 21:21
  • Mark do you have any idea where line 133 goes into? I'm trying to find the definition for scope.SQLDB().Exec(). [callback_create.go](https://github.com/jinzhu/gorm/blob/master/callback_create.go) – huggie Mar 06 '20 at 01:49
  • I'm trying to find where that extra single-quote is constructed. If I can figure it out I hope I can customize it. Is that the work of the dialect? – huggie Mar 06 '20 at 02:01
  • I think Exec() is part of go db interface, declared by database/sql and implemented by the db driver, in this case mysql. Gorm builds the insert sql statement with a call to scope.Raw(), passing values as 'placeholders', created by scope.AddToVars(..) function. AddToVars is where the check for *SqlExpr is done, so I assumed setting the column type would work. Also, it seems to call out to the dialect to do the quoting, and to bind each variable to the prepared statement. That quote function is wrapping each value in quotes. See dialect.go for the Dialect interface, with the Quote function. – Mark Mar 07 '20 at 05:00