# GeoSQL
This library provides access to geometric and geographic SQL functions as
implemented in SQL extensions such as PostGIS and SpatialLite.
This includes the entire suite of SQL/MM spatial functions,
non-standard functions that are found in commonly used GIS-enabled databases,
implementation-specific functions found in specific backends, as well as high-
level functions for features such as generating Mapbox vector tiles.
The goals of this library are:
* Ease: fast to get started, hide complexity where possible
* Portability: currently supports PostGIS and SpatialLite.
* Completeness: extensive support for GIS SQL functions, not just the most common ones.
* Clarity: Functions organized by their availability and standards compliance
* Utility: Provide out-of-the-box support for complete worfklows. Mapbox vector tile
generation is a good example: one call to `GeoSQL.PostGIS.VectorTiles.generate/6`
is enough to retrieve complete vector tiles based on any table in the database that has a geometry field.
Not-goals include:
* Having the fewest possible dependencies. Ecto adapters are pulled in as necessary,
along with other dependencies such as `Jason` in order to ease use.
## Usage
Add `GeoSQL` to your project by adding the following to the `deps` section in `mix.exs` (or equivalent):
```
{:geo_sql, "~> 0.1"}
```
Run the usual `mix deps.get`!
Full documentation can be generated locally with `mix docs`.
### Ecto Schemas
Ecto Schemas can have fields with the following values:
* GeoSQL.Geometry: this supports *all* geometry and geography types. It does no typechecking
beyond confirming it is a Geo-compatible type, making it a perfect "catch-all" generic
type for use in schemas.
* GeoSQL.Geometry.Point
* GeoSQL.Geometry.PointZ
* GeoSQL.Geometry.PointM
* GeoSQL.Geometry.PointZM
* GeoSQL.Geometry.LineString
* GeoSQL.Geometry.LineStringZ
* GeoSQL.Geometry.LineStringZM
* GeoSQL.Geometry.Polygon
* GeoSQL.Geometry.PolygonZ
* GeoSQL.Geometry.MultiPoint
* GeoSQL.Geometry.MultiPointZ
* GeoSQL.Geometry.MultiLineString
* GeoSQL.Geometry.MultiLineStringZ
* GeoSQL.Geometry.MultiLineStringZM
* GeoSQL.Geometry.MultiPolygon
* GeoSQL.Geometry.MultiPolygonZ
* GeoSQL.Geometry.GeometryCollection
Example:
```elixir
defmodule MyApp.GeoTable do
use Ecto.Schema
schema "specified_columns" do
field(:name, :string)
field(:geometry, GeoSQL.Geometry) # will match any Geo type
field(:point, GeoSQL.Geometry.Point) # will reject any non-Point data
field(:linestring, GeoSQL.Geometry.LineStringZ) # will reject any non-LineStringZ data
end
end
```
### Readying the Repo with `GeoSQL.init/1`
Once added to your project, an `Ecto.Repo` can be readied for use by calling
`GeoSQL.init/2`. This can be done once the repo has been started by implementing
the `init/2` callback in your repo module like this:
```elixir
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.Postgres
@impl true
def init(:supervisor, config) do
GeoSQL.init(__MODULE__, json: Jason)
{:ok, config}
end
def init(:runtime, config), do: {:ok, config}
end
```
For PostGIS, types are automatically added by `GeoSQL.init/2`. For this reason, call
`GeoSQL.init/2` *after* any other custom types are registered. The type extensions included
in `GeoSQL` are available via the `GeoSQL.PostGIS.Extension.extensions/0` function.
If migrations are failing, place a `GeoSQL.init/1` call in the top-level of the file the Repo is defined in:
```
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :geo_sql, adapter: Ecto.Adapters.Postgres
end
# For the migrations.
GeoSQL.init(MyApp.Repo)
```
This will ensure that any special types are defined and registered, though it
will still need to be called after the repo has been started.
Dynamic Ecto repositories are also supported, and `GeoSQL.init/1` can be
called after the call to `Repo.put_dynamic_repo/1` has completed.
### Macro usage
Once initialized, the wide array of macros can be used with `Ecto` queries:
```elixir
from(location in Location, select: Common.extent(location.geom, MyApp.Repo))
```
Some macros, such as `GeoSQL.Common.extent`, take an optional `Ecto.Repo` parameter.
This allows those macros to generate the correct SQL statements for the backend being used.
If no repo is passed to those functions, they assume PostGIS compatibility by default, though
this can be configured by adding this to `config.ex`:
```elixir
config :geo_sql, default_adapter: Ecto.Adapters.<PreferredAdapter>
```
Note that the value passed must be the literal repo module name. Passing in a variable
to which the repo was assigned will usually fail unless wrapped in a macro context, as
Ecto does all of its magic at compile-time, making the value of runtime variables unnavailable
for constructing queries (which is different from populating them with values). Usually this is
not an issue.
#### Composition
`GeoSQL` macros can also be freely composted and used together, such as this query which
uses a number of standard and PostGIS-specific features together:
```elixir
from(g in layer.source,
prefix: ^layer.prefix,
where:
bbox_intersects?(
field(g, ^columns.geometry),
MM2.transform(tile_envelope(^z, ^x, ^y), type(^layer.srid, Int4))
),
select: %{
name: ^layer.name,
geom:
as_mvt_geom(
field(g, ^columns.geometry),
MM2.transform(
tile_envelope(^z, ^x, ^y),
type(^layer.srid, Int4)
)
),
id: field(g, ^columns.id),
tags: field(g, ^columns.tags)
}
)
```
#### Queries needing geometry type casting
Sometimes queries will require casting to the database's native geometry type. Such casting is backend-specific, and so the `GeoSQL.QueryUtils.cast_to_geometry/2`
function which takes an `Ecto.Repo` is provided for portability.
The need to use it occurs when, for example, a query passes a geography type to a geometry
function in PostGIS, or the adapter (e.g. `postgrex`) can not automatically determine the type.
A common symptom of the latter case are errors noting that a binary was expected, and
the geometry struct provided was not serialized.
For example this query, where `lineA` and `lineB` are `Geometry.LineString` structs:
```elixir
from(location in Locations, select: MM2.intersection(^lineA, ^lineB))
```
may produce this error:
```
Postgrex expected a binary, got %Geometry.LineString{path: [[30, -90], [30, -91]], srid: 4326}. Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.
```
Casting one of the two type is usually enough to resolve this:
```elixir
from(location in Locations, select: MM2.intersection(QueryUtils.cast_to_geometry(^lineA, MyApp.Repo), ^lineB))
```
Note that using Ecto Schemas or referencing columns from a table avoids these issues, as the
database adapters can determine what the correct types are from that information.
### Queries Taking WKB data
Some functions take WKB-encoded data. If passing WKB blobs from the client-side to the backend,
wrap them using the `QueryUtils.wrap_wkb/2` macro, passing in the Ecto repo as the second parameter.
When used in a query, the return of this macro will need to be pinned (`^`) as it returns a value.
Example:
```elixir
from(g in GeoType,
select: g.linestring == MM2.linestring_from_wkb(^QueryUtils.wrap_wkb(wkb, MyApp.Repo), ^line.srid)
)
```
### Queries returning binary blobs instead of geometries
With certain backends (e.g. SQLite3), it is possible to craft queries that will return
binary blobs instead of decoded `Geo` structs.
In such cases, use `GeoSQL.QueryUtils.decode_geometry/2`:
```elixir
defmodule MyApp.Plots do
use GeoSQL.MM2
use GeoSQL.QueryUtils
def boundaries() do
from(location in Location, select: MM2.boundary(location.geom))
|> Repo.all()
|> QueryUtils.decode_geometry(Repo)
end
end
```
For backends that do not suffer from this (e.g. PostGIS), the call to `GeoSQL.decode_geometry`
is efficient, doing little more than a comparison of a single `atom` to determine no further
action needs to be taken.
### Module organization
Features are organized into modules by their availability and topic.
The v2 and v3 sets of standard SQL/MM functions for geospatial applications are found
in the `GeoSQL.MM2` and `GeoSQL.MM3` modules. Non-standardized but
commonly implemented functions are found in the `GeoSQL.Common` namespace, while
implementation-specific fuctions are found in namespaces indicating the target
database (e.g. `GeoSQL.PostGIS`).
Topological and 3D functions are found in `Topo` and `ThreeD` modules within this
hierarchy, as they less-used and/or have very similar names to more commonly used
SQL functions.
This helps make it clear what features your code relies on, allowing
one to audit feature usage for compability and avoid incompatible use
in the first place.
For example, if you using an older version of `PostGIS`, you may want to stick with only the
functions in the `GeoSQL.MM2` modules as the `GeoSQL.MM3` standard functions were
only implemented in later versions. Similarly, if targeting both `SpatialLite` and
`PostGIS`, the code should only use the standard features plus those in the `GeoSQL.Common`
modules.
To make this even easier, each of the top-level modules supports the `use` syntax which
pulls in their suite of features and introduces helpful aliases with one line in your code:
```elixir
use GeoSQL.MM2
use GeoSQL.MM3
def query() do
from(features in MyApp.Feature
select: %{
area_2d: MM2.area(features.geometry)`
area_3d: MM3.ThreeD.area(features.geometry)
}
)
end
```
### Mapbox Vector Tiles
`GeoSQL` can generate vector tiles using the Mapbox encoding directly from PostGIS databases.
It works with any table that has an id column, a column with geometry information, and a set of
tagged information such as names. The tag information is usually fetch as (or from) a `jsonb` data.
The `PostGIS.VectorTiles.generate/5` function takes a layer definition in the form of a list of
`PostGIS.VectorTiles.Layer` structs along with the tile coordinates and an `Ecto.Repo`:
```elixir
def tile(zoom, x, y) do
layers = [
%PostGIS.VectorTiles.Layer{
name: "pois",
source: "nodes",
columns: %{geometry: :geom, id: :node_id, tags: :tags}
},
%PostGIS.VectorTiles.Layer{
name: "buildings",
source: "buildings",
columns: %{geometry: :footprint, id: :id, tags: :tags}
}
]
PostGIS.VectorTiles.generate(MyApp.Repo, zoom, x, y, layers)
end
```
The resulting data can be loaded directly into map renderers such as `MapLibre` or `OpenLayers`
with the `MVT` vector tile layer format.
Database prefixes ("schemas" in PostgreSQL) are also supported both on the whole tile query
as well as per-layer.
## Building
To build and interact with the library locally:
git clone https://github.com/aseigo/geo_sql.git
cd geo_sql
mix deps.get
mix compile
iex -S mix
## Unit Tests
Unit tests currently assume a working PostGIS installation is available locally.
The URL for the test database is defined in `config/test.exs`.
**Note** that this database will be created and **dropped** on every run of the tests.
Do NOT point it to an existing database!
The migrations in `priv/repo/migrations` are run on each test run.
Running `mix test` will run tests along with setting up and tearing down the database.
This allows the tests to access the database in a known state each run.
Tests may be run continuously with `mix test.watch`.
### Running tests for a subset of backends
To limit which backends the tests are run against, set the `GEOSQL_TEST_BACKENDS` environment variable before running tests to a comma-separated list of backends.
Example:
```shell
# Run only the PostGIS tests.
GEOSQL_TEST_BACKENDS=pgsql mix test test/ecto_test.exs
```
Current the following backends are recognized:
* `pgsql`
* `sqlite3`
## Contributing
If you would like to contribute support for more functions (PostGIS and
SpatialLite both provide a frighteningly impressive amount of them!) or
support for other databases, do not hesitate to make a PR and the author
will review and merge in a timely fashion.
## Acknowledgements
This library began as a fork of the excellent `geo_postgis`, which the author
has used for many years, before growing into something rather larger. A big thank-you to felt.com for maintaining that
library over the course of many years.