Skip to content

Commit accc987

Browse files
jeroenbourgoiswojtekmach
authored andcommitted
Geometry types (#88)
1 parent 2c1a2a8 commit accc987

File tree

7 files changed

+196
-63
lines changed

7 files changed

+196
-63
lines changed

MARIAEX_COMPATIBILITY.md

-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ Queries:
3838

3939
* MyXQL represents bit type `B'101'` as `<<1::1, 0::0, 1::1>>` (`<<5::size(3)>>`), Mariaex represents it as `<<5>>`
4040

41-
* MyXQL does not support geometry types
42-
4341
* MyXQL does not support `:type_names`, `result_types`, `:decode`, `:encode_mapper`,
4442
`:decode_mapper`, `:include_table_name`, and `:binary_as` options
4543

README.md

+22-16
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,23 @@ See [Mariaex Compatibility](https://github.com/elixir-ecto/myxql/blob/master/MAR
8585
## Data representation
8686

8787
```
88-
MySQL Elixir
89-
----- ------
90-
NULL nil
91-
bool 1 | 0
92-
int 42
93-
float 42.0
94-
decimal #Decimal<42.0> *
95-
date ~D[2013-10-12] **
96-
time ~T[00:37:14]
97-
datetime ~N[2013-10-12 00:37:14] **, ***
98-
timestamp #DateTime<2013-10-12 00:37:14Z> ***
99-
json %{"foo" => "bar"} ****
100-
char "é"
101-
text "myxql"
102-
binary <<1, 2, 3>>
103-
bit <<1::size(1), 0::size(1)>>
88+
MySQL Elixir
89+
----- ------
90+
NULL nil
91+
bool 1 | 0
92+
int 42
93+
float 42.0
94+
decimal #Decimal<42.0> *
95+
date ~D[2013-10-12] **
96+
time ~T[00:37:14]
97+
datetime ~N[2013-10-12 00:37:14] **, ***
98+
timestamp ~U[2013-10-12 00:37:14Z] ***
99+
json %{"foo" => "bar"} ****
100+
char "é"
101+
text "myxql"
102+
binary <<1, 2, 3>>
103+
bit <<1::size(1), 0::size(1)>>
104+
point, polygon, ... %Geo.Point{coordinates: {0.0, 1.0}}, ... *****
104105
```
105106

106107
\* See [Decimal](https://github.com/ericmj/decimal)
@@ -112,6 +113,11 @@ bit <<1::size(1), 0::size(1)>>
112113
\*\*\*\* MySQL added a native JSON type in version 5.7.8, if you're using earlier versions,
113114
remember to use TEXT column for your JSON field.
114115

116+
\*\*\*\*\* Encoding/decoding between `Geo.*` structs and the OpenGIS WKB binary format is
117+
done using the [Geo](https://github.com/bryanjos/geo) package. If you're using MyXQL geometry
118+
types with Ecto and need to for example accept a WKT format as user input, consider implementing an
119+
[custom Ecto type](https://hexdocs.pm/ecto/Ecto.Type.html).
120+
115121
## JSON support
116122

117123
MyXQL comes with JSON support out of the box via the [Jason](https://github.com/michalmuskala/jason) library. To use it, add `:jason` to your dependencies:

lib/myxql/protocol/values.ex

+55-9
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ defmodule MyXQL.Protocol.Values do
8383
defp column_def_to_type(column_def(type: :mysql_type_string)), do: :binary
8484
defp column_def_to_type(column_def(type: :mysql_type_bit, length: length)), do: {:bit, length}
8585
defp column_def_to_type(column_def(type: :mysql_type_null)), do: :null
86+
defp column_def_to_type(column_def(type: :mysql_type_geometry)), do: :geometry
8687

8788
# Text values
8889

@@ -171,6 +172,10 @@ defmodule MyXQL.Protocol.Values do
171172
decode_bit(value, size)
172173
end
173174

175+
def decode_text_value(value, :geometry) do
176+
decode_geometry(value)
177+
end
178+
174179
# Binary values
175180

176181
def encode_binary_value(value)
@@ -216,6 +221,14 @@ defmodule MyXQL.Protocol.Values do
216221
{:mysql_type_tiny, <<0>>}
217222
end
218223

224+
def encode_binary_value(%Geo.Point{} = geo), do: encode_geometry(geo)
225+
def encode_binary_value(%Geo.MultiPoint{} = geo), do: encode_geometry(geo)
226+
def encode_binary_value(%Geo.LineString{} = geo), do: encode_geometry(geo)
227+
def encode_binary_value(%Geo.MultiLineString{} = geo), do: encode_geometry(geo)
228+
def encode_binary_value(%Geo.Polygon{} = geo), do: encode_geometry(geo)
229+
def encode_binary_value(%Geo.MultiPolygon{} = geo), do: encode_geometry(geo)
230+
def encode_binary_value(%Geo.GeometryCollection{} = geo), do: encode_geometry(geo)
231+
219232
def encode_binary_value(term) when is_list(term) or is_map(term) do
220233
string = json_library().encode!(term)
221234
{:mysql_type_var_string, encode_string_lenenc(string)}
@@ -225,6 +238,12 @@ defmodule MyXQL.Protocol.Values do
225238
raise ArgumentError, "query has invalid parameter #{inspect(other)}"
226239
end
227240

241+
defp encode_geometry(geo) do
242+
srid = geo.srid || 0
243+
binary = %{geo | srid: nil} |> Geo.WKB.encode!(:ndr) |> Base.decode16!()
244+
{:mysql_type_geometry, encode_string_lenenc(<<srid::uint4, binary::binary>>)}
245+
end
246+
228247
## Time/DateTime
229248

230249
# MySQL supports negative time and days, we don't.
@@ -308,7 +327,7 @@ defmodule MyXQL.Protocol.Values do
308327
end
309328

310329
defp decode_binary_row(<<r::bits>>, null_bitmap, [:binary | t], acc),
311-
do: decode_string_lenenc(r, null_bitmap, t, acc)
330+
do: decode_string_lenenc(r, null_bitmap, t, acc, & &1)
312331

313332
defp decode_binary_row(<<r::bits>>, null_bitmap, [:int1 | t], acc),
314333
do: decode_int1(r, null_bitmap, t, acc)
@@ -361,10 +380,19 @@ defmodule MyXQL.Protocol.Values do
361380
defp decode_binary_row(<<r::bits>>, null_bitmap, [{:bit, size} | t], acc),
362381
do: decode_bit(r, size, null_bitmap, t, acc)
363382

383+
defp decode_binary_row(<<r::bits>>, null_bitmap, [:geometry | t], acc),
384+
do: decode_string_lenenc(r, null_bitmap, t, acc, &decode_geometry/1)
385+
364386
defp decode_binary_row(<<>>, _null_bitmap, [], acc) do
365387
Enum.reverse(acc)
366388
end
367389

390+
# https://dev.mysql.com/doc/refman/8.0/en/gis-data-formats.html#gis-internal-format
391+
defp decode_geometry(<<srid::uint4, r::bits>>) do
392+
srid = if srid == 0, do: nil, else: srid
393+
r |> Base.encode16() |> Geo.WKB.decode!() |> Map.put(:srid, srid)
394+
end
395+
368396
defp decode_int1(<<v::int1, r::bits>>, null_bitmap, t, acc),
369397
do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
370398

@@ -510,18 +538,36 @@ defmodule MyXQL.Protocol.Values do
510538
}
511539
end
512540

513-
defp decode_string_lenenc(<<n::uint1, v::string(n), r::bits>>, null_bitmap, t, acc)
541+
defp decode_string_lenenc(<<n::uint1, v::string(n), r::bits>>, null_bitmap, t, acc, decoder)
514542
when n < 251,
515-
do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
543+
do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc])
516544

517-
defp decode_string_lenenc(<<0xFC, n::uint2, v::string(n), r::bits>>, null_bitmap, t, acc),
518-
do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
545+
defp decode_string_lenenc(
546+
<<0xFC, n::uint2, v::string(n), r::bits>>,
547+
null_bitmap,
548+
t,
549+
acc,
550+
decoder
551+
),
552+
do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc])
519553

520-
defp decode_string_lenenc(<<0xFD, n::uint3, v::string(n), r::bits>>, null_bitmap, t, acc),
521-
do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
554+
defp decode_string_lenenc(
555+
<<0xFD, n::uint3, v::string(n), r::bits>>,
556+
null_bitmap,
557+
t,
558+
acc,
559+
decoder
560+
),
561+
do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc])
522562

523-
defp decode_string_lenenc(<<0xFE, n::uint8, v::string(n), r::bits>>, null_bitmap, t, acc),
524-
do: decode_binary_row(r, null_bitmap >>> 1, t, [v | acc])
563+
defp decode_string_lenenc(
564+
<<0xFE, n::uint8, v::string(n), r::bits>>,
565+
null_bitmap,
566+
t,
567+
acc,
568+
decoder
569+
),
570+
do: decode_binary_row(r, null_bitmap >>> 1, t, [decoder.(v) | acc])
525571

526572
defp decode_json(<<n::uint1, v::string(n), r::bits>>, null_bitmap, t, acc) when n < 251,
527573
do: decode_binary_row(r, null_bitmap >>> 1, t, [decode_json(v) | acc])

mix.exs

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ defmodule MyXQL.MixProject do
5151
{:db_connection, "~> 2.0", db_connection_opts()},
5252
{:decimal, "~> 1.6"},
5353
{:jason, "~> 1.0", optional: true},
54+
{:geo, "~> 3.3"},
5455
{:binpp, ">= 0.0.0", only: [:dev, :test]},
5556
{:dialyxir, "~> 1.0-rc", only: :dev, runtime: false},
5657
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},

mix.lock

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"earmark": {:hex, :earmark, "1.4.0", "397e750b879df18198afc66505ca87ecf6a96645545585899f6185178433cc09", [:mix], [], "hexpm"},
1010
"erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm"},
1111
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
12+
"geo": {:hex, :geo, "3.3.2", "30c7b458bcb0ab1ca73a997b26d22c68643d9ffe1726e475e0759499b6024cff", [:mix], [], "hexpm"},
1213
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
1314
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
1415
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},

test/myxql/protocol/values_test.exs

+92-35
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,64 @@ defmodule MyXQL.Protocol.ValueTest do
273273
test "CHAR", c do
274274
assert_roundtrip(c, "my_char", "é")
275275
end
276+
277+
@tag geometry: true
278+
test "POINT", c do
279+
assert_roundtrip(c, "my_point", %Geo.Point{coordinates: {0, 0}})
280+
end
281+
282+
@tag geometry: true
283+
test "POINT with SRID", c do
284+
assert_roundtrip(c, "my_point", %Geo.Point{coordinates: {0, 0}, srid: 4326})
285+
end
286+
287+
@tag geometry: true
288+
test "LINESTRING", c do
289+
assert_roundtrip(c, "my_linestring", %Geo.LineString{
290+
coordinates: [{30, 10}, {10, 30}, {40, 40}]
291+
})
292+
end
293+
294+
@tag geometry: true
295+
test "MULTIPOINT", c do
296+
assert_roundtrip(c, "my_multipoint", %Geo.MultiPoint{
297+
coordinates: [{1.0, 1.0}, {2.0, 2.0}]
298+
})
299+
end
300+
301+
@tag geometry: true
302+
test "MULTILINESTRING", c do
303+
assert_roundtrip(c, "my_multilinestring", %Geo.MultiLineString{
304+
coordinates: [[{10, 10}, {20, 20}], [{15, 15}, {30, 15}]]
305+
})
306+
end
307+
308+
@tag geometry: true
309+
test "POLYGON", c do
310+
assert_roundtrip(c, "my_polygon", %Geo.Polygon{
311+
coordinates: [[{30, 10}, {40, 40}, {20, 40}, {10, 20}, {30, 10}]]
312+
})
313+
end
314+
315+
@tag geometry: true
316+
test "MULTIPOLYGON", c do
317+
assert_roundtrip(c, "my_multipolygon", %Geo.MultiPolygon{
318+
coordinates: [
319+
[[{40, 40}, {20, 45}, {45, 30}, {40, 40}]],
320+
[
321+
[{20, 35}, {10, 30}, {10, 10}, {30, 5}, {45, 20}, {20, 35}],
322+
[{30, 20}, {20, 15}, {20, 25}, {30, 20}]
323+
]
324+
]
325+
})
326+
end
327+
328+
@tag geometry: true
329+
test "GEOMETRYCOLLECTION", c do
330+
assert_roundtrip(c, "my_geometrycollection", %Geo.GeometryCollection{
331+
geometries: [%Geo.Point{coordinates: {54.1745659, 15.5398456}}]
332+
})
333+
end
276334
end
277335
end
278336

@@ -352,41 +410,7 @@ defmodule MyXQL.Protocol.ValueTest do
352410

353411
defp insert(%{protocol: :text} = c, fields, values) when is_list(fields) and is_list(values) do
354412
fields = Enum.map_join(fields, ", ", &"`#{&1}`")
355-
356-
values =
357-
Enum.map_join(values, ", ", fn
358-
nil ->
359-
"NULL"
360-
361-
true ->
362-
"TRUE"
363-
364-
false ->
365-
"FALSE"
366-
367-
%DateTime{} = datetime ->
368-
"'#{NaiveDateTime.to_iso8601(datetime)}'"
369-
370-
list when is_list(list) ->
371-
"'#{Jason.encode!(list)}'"
372-
373-
%_{} = struct ->
374-
"'#{struct}'"
375-
376-
map when is_map(map) ->
377-
"'#{Jason.encode!(map)}'"
378-
379-
value when is_binary(value) ->
380-
"'#{value}'"
381-
382-
value when is_bitstring(value) ->
383-
size = bit_size(value)
384-
<<value::size(size)>> = value
385-
"B'#{Integer.to_string(value, 2)}'"
386-
387-
value ->
388-
"'#{value}'"
389-
end)
413+
values = Enum.map_join(values, ", ", &encode_text/1)
390414

391415
%MyXQL.Result{last_insert_id: id} =
392416
query!(c, "INSERT INTO test_types (#{fields}) VALUES (#{values})")
@@ -407,6 +431,39 @@ defmodule MyXQL.Protocol.ValueTest do
407431
insert(c, [field], [value])
408432
end
409433

434+
defp encode_text(nil), do: "NULL"
435+
defp encode_text(true), do: "TRUE"
436+
defp encode_text(false), do: "FALSE"
437+
defp encode_text(%DateTime{} = datetime), do: "'#{NaiveDateTime.to_iso8601(datetime)}'"
438+
defp encode_text(%Geo.Point{} = geo), do: encode_text_geo(geo)
439+
defp encode_text(%Geo.LineString{} = geo), do: encode_text_geo(geo)
440+
defp encode_text(%Geo.Polygon{} = geo), do: encode_text_geo(geo)
441+
defp encode_text(%Geo.MultiPoint{} = geo), do: encode_text_geo(geo)
442+
defp encode_text(%Geo.MultiLineString{} = geo), do: encode_text_geo(geo)
443+
defp encode_text(%Geo.MultiPolygon{} = geo), do: encode_text_geo(geo)
444+
defp encode_text(%Geo.GeometryCollection{} = geo), do: encode_text_geo(geo)
445+
defp encode_text(%_{} = struct), do: "'#{struct}'"
446+
defp encode_text(map) when is_map(map), do: "'#{Jason.encode!(map)}'"
447+
defp encode_text(list) when is_list(list), do: "'#{Jason.encode!(list)}'"
448+
defp encode_text(value) when is_binary(value), do: "'#{value}'"
449+
450+
defp encode_text(value) when is_bitstring(value) do
451+
size = bit_size(value)
452+
<<value::size(size)>> = value
453+
"B'#{Integer.to_string(value, 2)}'"
454+
end
455+
456+
defp encode_text(value), do: "'#{value}'"
457+
458+
defp encode_text_geo(value) do
459+
if srid = value.srid do
460+
value = %{value | srid: nil}
461+
"ST_GeomFromText('#{Geo.WKT.encode!(value)}', #{srid})"
462+
else
463+
"ST_GeomFromText('#{Geo.WKT.encode!(value)}')"
464+
end
465+
end
466+
410467
defp get(c, fields, id) when is_list(fields) do
411468
fields = Enum.map_join(fields, ", ", &"`#{&1}`")
412469
statement = "SELECT #{fields} FROM test_types WHERE id = '#{id}'"

0 commit comments

Comments
 (0)