Skip to content

Commit c3ff2cd

Browse files
authored
Add MyXQL.Protocol.GeometryCodec (#208)
1 parent 6b90f71 commit c3ff2cd

4 files changed

Lines changed: 94 additions & 35 deletions

File tree

README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,22 @@ config :myxql, :json_library, SomeJSONModule
135135

136136
## Geometry support
137137

138-
MyXQL comes with Geometry types support via the [Geo](https://github.com/bryanjos/geo) package.
138+
MyXQL supports data stored in `geometry` columns with the help of external geometry libraries such as `geo`.
139139

140-
To use it, add `:geo` to your dependencies:
140+
If using a library other than `geo`, `myxql` can be configured to use a different codec for encoding
141+
and decoding in the application config, e.g.:
141142

142143
```elixir
143-
{:geo, "~> 3.3"}
144+
config :myxql, geometry_codec: GeoSQL.MySQL.Codec
144145
```
145146

146-
Note, some structs like `%Geo.PointZ{}` does not have equivalent on the MySQL server side and thus
147-
shouldn't be used.
147+
If using the `geo` library, no explicit configuration is required.
148+
149+
Note: some geometry types available in these libraries, such as `PointZ`, are not supported by MySQL and thus
150+
should not be used.
148151

149152
If you're using MyXQL geometry types with Ecto and need to for example accept a WKT format as user
150-
input, consider implementing an [custom Ecto type](https://hexdocs.pm/ecto/Ecto.Type.html).
153+
input, consider implementing a [custom Ecto type](https://hexdocs.pm/ecto/Ecto.Type.html).
151154

152155
## UTC required
153156

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule MyXQL.Protocol.GeometryCodec do
2+
@type no_srid() :: 0
3+
@type some_srid() :: pos_integer()
4+
5+
@callback encode(struct()) :: {srid :: integer(), wkb :: binary()} | :unknown
6+
@callback decode(srid :: no_srid() | some_srid(), wkb :: binary()) :: struct() | :unknown
7+
8+
import MyXQL.Protocol.Types
9+
10+
@doc false
11+
def do_encode(struct) do
12+
with codec when not is_nil(codec) <- geometry_codec(),
13+
{srid, wkb} <- codec.encode(struct) do
14+
{
15+
:mysql_type_var_string,
16+
MyXQL.Protocol.Types.encode_string_lenenc(<<srid::uint4(), wkb::binary>>)
17+
}
18+
else
19+
_ -> :unknown
20+
end
21+
end
22+
23+
@doc false
24+
def do_decode(srid, wkb) do
25+
codec = geometry_codec()
26+
27+
if codec != nil do
28+
codec.decode(srid, wkb)
29+
else
30+
:unknown
31+
end
32+
end
33+
34+
default_codec = if Code.ensure_loaded?(Geo), do: MyXQL.Protocol.GeometryCodec.Geo, else: nil
35+
36+
defp geometry_codec do
37+
Application.get_env(:myxql, :geometry_codec, unquote(default_codec))
38+
end
39+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
if Code.ensure_loaded?(Geo) do
2+
defmodule MyXQL.Protocol.GeometryCodec.Geo do
3+
@behaviour MyXQL.Protocol.GeometryCodec
4+
5+
supported_structs = [
6+
Geo.Point,
7+
Geo.GeometryCollection,
8+
Geo.LineString,
9+
Geo.MultiPoint,
10+
Geo.MultiLineString,
11+
Geo.MultiPolygon,
12+
Geo.Polygon
13+
]
14+
15+
def encode(%x{} = geo) when x in unquote(supported_structs) do
16+
srid = geo.srid || 0
17+
wkb = %{geo | srid: nil} |> Geo.WKB.encode_to_iodata(:ndr) |> IO.iodata_to_binary()
18+
{srid, wkb}
19+
end
20+
21+
def encode(_), do: :unknown
22+
23+
def decode(0, wkb), do: Geo.WKB.decode!(wkb)
24+
def decode(srid, wkb), do: Geo.WKB.decode!(wkb) |> Map.put(:srid, srid)
25+
end
26+
end

lib/myxql/protocol/values.ex

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -250,14 +250,16 @@ defmodule MyXQL.Protocol.Values do
250250
{:mysql_type_tiny, <<0>>}
251251
end
252252

253-
if Code.ensure_loaded?(Geo) do
254-
def encode_binary_value(%Geo.Point{} = geo), do: encode_geometry(geo)
255-
def encode_binary_value(%Geo.MultiPoint{} = geo), do: encode_geometry(geo)
256-
def encode_binary_value(%Geo.LineString{} = geo), do: encode_geometry(geo)
257-
def encode_binary_value(%Geo.MultiLineString{} = geo), do: encode_geometry(geo)
258-
def encode_binary_value(%Geo.Polygon{} = geo), do: encode_geometry(geo)
259-
def encode_binary_value(%Geo.MultiPolygon{} = geo), do: encode_geometry(geo)
260-
def encode_binary_value(%Geo.GeometryCollection{} = geo), do: encode_geometry(geo)
253+
def encode_binary_value(struct) when is_struct(struct) do
254+
# see if it is a geometry struct
255+
case MyXQL.Protocol.GeometryCodec.do_encode(struct) do
256+
:unknown ->
257+
string = json_library().encode!(struct)
258+
{:mysql_type_var_string, encode_string_lenenc(string)}
259+
260+
encoded ->
261+
encoded
262+
end
261263
end
262264

263265
def encode_binary_value(term) when is_list(term) or is_map(term) do
@@ -269,14 +271,6 @@ defmodule MyXQL.Protocol.Values do
269271
raise ArgumentError, "query has invalid parameter #{inspect(other)}"
270272
end
271273

272-
if Code.ensure_loaded?(Geo) do
273-
defp encode_geometry(geo) do
274-
srid = geo.srid || 0
275-
binary = %{geo | srid: nil} |> Geo.WKB.encode_to_iodata(:ndr) |> IO.iodata_to_binary()
276-
{:mysql_type_var_string, encode_string_lenenc(<<srid::uint4(), binary::binary>>)}
277-
end
278-
end
279-
280274
## Time/DateTime
281275

282276
# MySQL supports negative time and days, we don't.
@@ -423,21 +417,18 @@ defmodule MyXQL.Protocol.Values do
423417
Enum.reverse(acc)
424418
end
425419

426-
if Code.ensure_loaded?(Geo) do
427-
# https://dev.mysql.com/doc/refman/8.0/en/gis-data-formats.html#gis-internal-format
428-
defp decode_geometry(<<srid::uint4(), r::bits>>) do
429-
srid = if srid == 0, do: nil, else: srid
430-
r |> Geo.WKB.decode!() |> Map.put(:srid, srid)
431-
end
432-
else
433-
defp decode_geometry(_) do
434-
raise """
435-
encoding/decoding geometry types requires :geo package, add:
420+
defp decode_geometry(<<srid::uint4(), data::bits>>) do
421+
case MyXQL.Protocol.GeometryCodec.do_decode(srid, data) do
422+
:unknown ->
423+
raise """
424+
Decoding geometry types requires a geometry library with a MySQL codec. Add a library such
425+
as `geo` or `geometry` as a dependency, and register its codec in the application configuration:
436426
437-
{:geo, "~> 3.4"}
427+
config :myxql, wkb_decoder: {Geometry, :from_wkb!}
428+
"""
438429

439-
to your mix.exs and run `mix deps.compile --force myxql`.
440-
"""
430+
decoded ->
431+
decoded
441432
end
442433
end
443434

0 commit comments

Comments
 (0)