diff --git a/README.md b/README.md index bb147be..1196092 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/myxql/protocol/geometry_codec.ex b/lib/myxql/protocol/geometry_codec.ex new file mode 100644 index 0000000..2b3a6f7 --- /dev/null +++ b/lib/myxql/protocol/geometry_codec.ex @@ -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(<>) + } + 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 diff --git a/lib/myxql/protocol/geometry_codec/geo.ex b/lib/myxql/protocol/geometry_codec/geo.ex new file mode 100644 index 0000000..dcdd346 --- /dev/null +++ b/lib/myxql/protocol/geometry_codec/geo.ex @@ -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 diff --git a/lib/myxql/protocol/values.ex b/lib/myxql/protocol/values.ex index 3baf5ae..514fb2c 100644 --- a/lib/myxql/protocol/values.ex +++ b/lib/myxql/protocol/values.ex @@ -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 @@ -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(<>)} - end - end - ## Time/DateTime # MySQL supports negative time and days, we don't. @@ -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(<>) 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(<>) 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