Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,22 @@ config :myxql, :json_library, SomeJSONModule

## Geometry support

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

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

```elixir
{:geo, "~> 3.3"}
config :myxql, geometry_codec: GeoSQL.MySQL.Codec
```

Note, some structs like `%Geo.PointZ{}` does not have equivalent on the MySQL server side and thus
shouldn't be used.
If using the `geo` library, no explicit configuration is required.

Note: some geometry types available in these libraries, such as `PointZ`, are not supported by MySQL and thus
should not be used.

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

## UTC required

Expand Down
39 changes: 39 additions & 0 deletions lib/myxql/protocol/geometry_codec.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule MyXQL.Protocol.GeometryCodec do
@type no_srid() :: 0
@type some_srid() :: pos_integer()

@callback encode(struct()) :: {srid :: integer(), wkb :: binary()} | :unknown
@callback decode(srid :: no_srid() | some_srid(), wkb :: binary()) :: struct() | :unknown

import MyXQL.Protocol.Types

@doc false
def do_encode(struct) do
with codec when not is_nil(codec) <- geometry_codec(),
{srid, wkb} <- codec.encode(struct) do
{
:mysql_type_var_string,
MyXQL.Protocol.Types.encode_string_lenenc(<<srid::uint4(), wkb::binary>>)
}
else
_ -> :unknown
end
end

@doc false
def do_decode(srid, wkb) do
codec = geometry_codec()

if codec != nil do
codec.decode(srid, wkb)
else
:unknown
end
end

default_codec = if Code.ensure_loaded?(Geo), do: MyXQL.Protocol.GeometryCodec.Geo, else: nil

defp geometry_codec do
Application.get_env(:myxql, :geometry_codec, unquote(default_codec))
end
end
26 changes: 26 additions & 0 deletions lib/myxql/protocol/geometry_codec/geo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
if Code.ensure_loaded?(Geo) do
defmodule MyXQL.Protocol.GeometryCodec.Geo do
@behaviour MyXQL.Protocol.GeometryCodec

supported_structs = [
Geo.Point,
Geo.GeometryCollection,
Geo.LineString,
Geo.MultiPoint,
Geo.MultiLineString,
Geo.MultiPolygon,
Geo.Polygon
]

def encode(%x{} = geo) when x in unquote(supported_structs) do
srid = geo.srid || 0
wkb = %{geo | srid: nil} |> Geo.WKB.encode_to_iodata(:ndr) |> IO.iodata_to_binary()
{srid, wkb}
end

def encode(_), do: :unknown

def decode(0, wkb), do: Geo.WKB.decode!(wkb)
def decode(srid, wkb), do: Geo.WKB.decode!(wkb) |> Map.put(:srid, srid)
end
end
49 changes: 20 additions & 29 deletions lib/myxql/protocol/values.ex
Original file line number Diff line number Diff line change
Expand Up @@ -250,14 +250,16 @@ defmodule MyXQL.Protocol.Values do
{:mysql_type_tiny, <<0>>}
end

if Code.ensure_loaded?(Geo) do
def encode_binary_value(%Geo.Point{} = geo), do: encode_geometry(geo)
def encode_binary_value(%Geo.MultiPoint{} = geo), do: encode_geometry(geo)
def encode_binary_value(%Geo.LineString{} = geo), do: encode_geometry(geo)
def encode_binary_value(%Geo.MultiLineString{} = geo), do: encode_geometry(geo)
def encode_binary_value(%Geo.Polygon{} = geo), do: encode_geometry(geo)
def encode_binary_value(%Geo.MultiPolygon{} = geo), do: encode_geometry(geo)
def encode_binary_value(%Geo.GeometryCollection{} = geo), do: encode_geometry(geo)
def encode_binary_value(struct) when is_struct(struct) do
# see if it is a geometry struct
case MyXQL.Protocol.GeometryCodec.do_encode(struct) do
:unknown ->
string = json_library().encode!(struct)
{:mysql_type_var_string, encode_string_lenenc(string)}

encoded ->
encoded
end
end

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

if Code.ensure_loaded?(Geo) do
defp encode_geometry(geo) do
srid = geo.srid || 0
binary = %{geo | srid: nil} |> Geo.WKB.encode_to_iodata(:ndr) |> IO.iodata_to_binary()
{:mysql_type_var_string, encode_string_lenenc(<<srid::uint4(), binary::binary>>)}
end
end

## Time/DateTime

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

if Code.ensure_loaded?(Geo) do
# https://dev.mysql.com/doc/refman/8.0/en/gis-data-formats.html#gis-internal-format
defp decode_geometry(<<srid::uint4(), r::bits>>) do
srid = if srid == 0, do: nil, else: srid
r |> Geo.WKB.decode!() |> Map.put(:srid, srid)
end
else
defp decode_geometry(_) do
raise """
encoding/decoding geometry types requires :geo package, add:
defp decode_geometry(<<srid::uint4(), data::bits>>) do
case MyXQL.Protocol.GeometryCodec.do_decode(srid, data) do
:unknown ->
raise """
Decoding geometry types requires a geometry library with a MySQL codec. Add a library such
as `geo` or `geometry` as a dependency, and register its codec in the application configuration:

{:geo, "~> 3.4"}
config :myxql, wkb_decoder: {Geometry, :from_wkb!}
"""

to your mix.exs and run `mix deps.compile --force myxql`.
"""
decoded ->
decoded
end
end

Expand Down