defmodule QuackDB.Spatial do @moduledoc """ SQL helpers for DuckDB's spatial extension. These helpers return iodata fragments for raw DuckDB SQL. Load the spatial extension before using `ST_*` functions: QuackDB.query!(conn, QuackDB.Spatial.load()) Geometry values returned by DuckDB's Quack protocol decode as WKB-compatible binaries. """ @doc "Builds an `INSTALL spatial;` statement." @spec install() :: iodata() def install, do: QuackDB.SQL.install(:spatial) @doc "Builds a `LOAD spatial;` statement." @spec load() :: iodata() def load, do: QuackDB.SQL.load(:spatial) @doc "Builds `ST_Point(x, y)`." @spec point(number(), number()) :: iodata() def point(x, y), do: call("ST_Point", [literal!(x), literal!(y)]) @doc "Builds `ST_AsWKB(geometry)`." @spec as_wkb(iodata()) :: iodata() def as_wkb(geometry), do: call("ST_AsWKB", [geometry]) @doc "Builds `ST_AsHEXWKB(geometry)`." @spec as_hex_wkb(iodata()) :: iodata() def as_hex_wkb(geometry), do: call("ST_AsHEXWKB", [geometry]) @doc "Builds `ST_AsText(geometry)`." @spec as_text(iodata()) :: iodata() def as_text(geometry), do: call("ST_AsText", [geometry]) @doc "Builds `ST_AsGeoJSON(geometry)`." @spec as_geojson(iodata()) :: iodata() def as_geojson(geometry), do: call("ST_AsGeoJSON", [geometry]) @doc "Builds `ST_Intersects(left, right)`." @spec intersects(iodata(), iodata()) :: iodata() def intersects(left, right), do: call("ST_Intersects", [left, right]) @doc "Builds `ST_MakeEnvelope(min_x, min_y, max_x, max_y)`." @spec envelope(number(), number(), number(), number()) :: iodata() def envelope(min_x, min_y, max_x, max_y) do call("ST_MakeEnvelope", [literal!(min_x), literal!(min_y), literal!(max_x), literal!(max_y)]) end @doc "Builds `ST_GeomFromWKB(wkb)`." @spec geom_from_wkb(binary() | iodata()) :: iodata() def geom_from_wkb(wkb) when is_binary(wkb), do: call("ST_GeomFromWKB", [literal!({:blob, wkb})]) def geom_from_wkb(wkb), do: call("ST_GeomFromWKB", [wkb]) @doc "Builds `ST_GeomFromText(wkt)`." @spec geom_from_text(String.t()) :: iodata() def geom_from_text(wkt) when is_binary(wkt), do: call("ST_GeomFromText", [literal!(wkt)]) defp call(function, args) do [function, "(", Enum.intersperse(args, ", "), ")"] end defp literal!(value) do case QuackDB.SQL.literal(value) do {:ok, literal} -> literal {:error, error} -> raise error end end end