Skip to content

Commit 98c4234

Browse files
committed
ewkb sql.Scanner and driver.Value
1 parent 347e52a commit 98c4234

File tree

7 files changed

+1710
-38
lines changed

7 files changed

+1710
-38
lines changed

encoding/ewkb/scanner.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package ewkb
2+
3+
import (
4+
"database/sql"
5+
"database/sql/driver"
6+
"encoding/hex"
7+
"fmt"
8+
9+
"github.com/paulmach/orb"
10+
"github.com/paulmach/orb/encoding/internal/wkbcommon"
11+
)
12+
13+
var (
14+
_ sql.Scanner = &GeometryScanner{}
15+
_ driver.Value = value{}
16+
)
17+
18+
// GeometryScanner is a thing that can scan in sql query results.
19+
// It can be used as a scan destination:
20+
//
21+
// var s wkb.GeometryScanner
22+
// err := db.QueryRow("SELECT latlon FROM foo WHERE id=?", id).Scan(&s)
23+
// ...
24+
// if s.Valid {
25+
// // use s.Geometry
26+
// } else {
27+
// // NULL value
28+
// }
29+
type GeometryScanner struct {
30+
g interface{}
31+
SRID int
32+
Geometry orb.Geometry
33+
Valid bool // Valid is true if the geometry is not NULL
34+
}
35+
36+
// Scanner will return a GeometryScanner that can scan sql query results.
37+
// The geometryScanner.Geometry attribute will be set to the value.
38+
// If g is non-nil, it MUST be a pointer to an orb.Geometry
39+
// type like a Point or LineString. In that case the value will be written to
40+
// g and the Geometry attribute.
41+
//
42+
// var p orb.Point
43+
// err := db.QueryRow("SELECT latlon FROM foo WHERE id=?", id).Scan(wkb.Scanner(&p))
44+
// ...
45+
// // use p
46+
//
47+
// If the value may be null check Valid first:
48+
//
49+
// var point orb.Point
50+
// s := wkb.Scanner(&point)
51+
// err := db.QueryRow("SELECT latlon FROM foo WHERE id=?", id).Scan(&s)
52+
// ...
53+
// if s.Valid {
54+
// // use p
55+
// } else {
56+
// // NULL value
57+
// }
58+
//
59+
// Scanning directly from MySQL columns is supported. By default MySQL returns geometry
60+
// data as WKB but prefixed with a 4 byte SRID. To support this, if the data is not
61+
// valid WKB, the code will strip the first 4 bytes and try again.
62+
// This works for most use cases.
63+
func Scanner(g interface{}) *GeometryScanner {
64+
return &GeometryScanner{g: g}
65+
}
66+
67+
// Scan will scan the input []byte data into a geometry.
68+
// This could be into the orb geometry type pointer or, if nil,
69+
// the scanner.Geometry attribute.
70+
func (s *GeometryScanner) Scan(d interface{}) error {
71+
s.Geometry = nil
72+
s.Valid = false
73+
74+
if d == nil {
75+
return nil
76+
}
77+
78+
data, ok := d.([]byte)
79+
if !ok {
80+
return ErrUnsupportedDataType
81+
}
82+
83+
if data == nil {
84+
return nil
85+
}
86+
87+
// go-pg will return ST_AsBinary(*) data as `\xhexencoded` which
88+
// needs to be converted to true binary for further decoding.
89+
// Code detects the \x prefix and then converts the rest from Hex to binary.
90+
if len(data) > 2 && data[0] == byte('\\') && data[1] == byte('x') {
91+
n, err := hex.Decode(data, data[2:])
92+
if err != nil {
93+
return fmt.Errorf("thought the data was hex, but it is not: %v", err)
94+
}
95+
data = data[:n]
96+
}
97+
98+
switch g := s.g.(type) {
99+
case nil:
100+
m, srid, err := Unmarshal(data)
101+
if err != nil {
102+
return err
103+
}
104+
105+
s.SRID = srid
106+
s.Geometry = m
107+
s.Valid = true
108+
return nil
109+
case *orb.Point:
110+
p, srid, err := wkbcommon.ScanPoint(data)
111+
if err != nil {
112+
return mapCommonError(err)
113+
}
114+
115+
*g = p
116+
s.SRID = srid
117+
s.Geometry = p
118+
s.Valid = true
119+
return nil
120+
case *orb.MultiPoint:
121+
m, srid, err := wkbcommon.ScanMultiPoint(data)
122+
if err != nil {
123+
return mapCommonError(err)
124+
}
125+
126+
*g = m
127+
s.SRID = srid
128+
s.Geometry = m
129+
s.Valid = true
130+
return nil
131+
case *orb.LineString:
132+
l, srid, err := wkbcommon.ScanLineString(data)
133+
if err != nil {
134+
return mapCommonError(err)
135+
}
136+
137+
*g = l
138+
s.SRID = srid
139+
s.Geometry = l
140+
s.Valid = true
141+
return nil
142+
case *orb.MultiLineString:
143+
m, srid, err := wkbcommon.ScanMultiLineString(data)
144+
if err != nil {
145+
return mapCommonError(err)
146+
}
147+
148+
*g = m
149+
s.SRID = srid
150+
s.Geometry = m
151+
s.Valid = true
152+
return nil
153+
case *orb.Ring:
154+
m, srid, err := Unmarshal(data)
155+
if err != nil {
156+
return err
157+
}
158+
159+
if p, ok := m.(orb.Polygon); ok && len(p) == 1 {
160+
*g = p[0]
161+
s.SRID = srid
162+
s.Geometry = p[0]
163+
s.Valid = true
164+
return nil
165+
}
166+
167+
return ErrIncorrectGeometry
168+
case *orb.Polygon:
169+
p, srid, err := wkbcommon.ScanPolygon(data)
170+
if err != nil {
171+
return mapCommonError(err)
172+
}
173+
174+
*g = p
175+
s.SRID = srid
176+
s.Geometry = p
177+
s.Valid = true
178+
return nil
179+
case *orb.MultiPolygon:
180+
m, srid, err := wkbcommon.ScanMultiPolygon(data)
181+
if err != nil {
182+
return mapCommonError(err)
183+
}
184+
185+
*g = m
186+
s.SRID = srid
187+
s.Geometry = m
188+
s.Valid = true
189+
return nil
190+
case *orb.Collection:
191+
c, srid, err := wkbcommon.ScanCollection(data)
192+
if err != nil {
193+
return mapCommonError(err)
194+
}
195+
196+
*g = c
197+
s.SRID = srid
198+
s.Geometry = c
199+
s.Valid = true
200+
return nil
201+
case *orb.Bound:
202+
m, srid, err := Unmarshal(data)
203+
if err != nil {
204+
return err
205+
}
206+
207+
b := m.Bound()
208+
*g = b
209+
s.SRID = srid
210+
s.Geometry = b
211+
s.Valid = true
212+
return nil
213+
}
214+
215+
return ErrIncorrectGeometry
216+
}
217+
218+
type value struct {
219+
srid int
220+
v orb.Geometry
221+
}
222+
223+
// Value will create a driver.Valuer that will WKB the geometry
224+
// into the database query.
225+
func Value(g orb.Geometry, srid int) driver.Valuer {
226+
return value{srid: srid, v: g}
227+
228+
}
229+
230+
func (v value) Value() (driver.Value, error) {
231+
val, err := Marshal(v.v, v.srid)
232+
if val == nil {
233+
return nil, err
234+
}
235+
return val, err
236+
}

0 commit comments

Comments
 (0)