Skip to content

Commit 15ba005

Browse files
Add GeoPackage support (#456)
* Add GeoPackage support * Define GeoPackage dialect and rework the code accordingly * Fix comments from review * Filter GeoPackage tables in Alembic helpers * Create a view instead of a table for spatial_ref_sys * Fix docs
1 parent 8644ac7 commit 15ba005

25 files changed

+1387
-175
lines changed

doc/admin.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.. _admin:
2+
3+
Administration
4+
==============
5+
6+
.. automodule:: geoalchemy2.admin
7+
:members:
8+
:private-members:
9+
:show-inheritance:
10+
11+
12+
Common objects
13+
--------------
14+
15+
.. automodule:: geoalchemy2.admin.dialects.common
16+
:members:
17+
:private-members:
18+
:show-inheritance:
19+
20+
PostgreSQL-specific objects
21+
---------------------------
22+
23+
.. automodule:: geoalchemy2.admin.dialects.postgresql
24+
:members:
25+
:private-members:
26+
:show-inheritance:
27+
28+
MySQL-specific objects
29+
---------------------------
30+
31+
.. automodule:: geoalchemy2.admin.dialects.mysql
32+
:members:
33+
:private-members:
34+
:show-inheritance:
35+
36+
SQLite-specific objects
37+
---------------------------
38+
39+
.. automodule:: geoalchemy2.admin.dialects.sqlite
40+
:members:
41+
:private-members:
42+
:show-inheritance:
43+
44+
GeoPackage-specific objects
45+
---------------------------
46+
47+
.. automodule:: geoalchemy2.admin.dialects.geopackage
48+
:members:
49+
:private-members:
50+
:show-inheritance:

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Reference Documentation
108108
.. toctree::
109109
:maxdepth: 1
110110

111+
admin
111112
types
112113
elements
113114
spatial_functions

doc/spatialite_tutorial.rst

Lines changed: 77 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,37 @@ GeoAlchemy 2's main target is PostGIS. But GeoAlchemy 2 also supports SpatiaLite
77
extension to SQLite. This tutorial describes how to use GeoAlchemy 2 with SpatiaLite. It's based on
88
the :ref:`orm_tutorial`, which you may want to read first.
99

10+
.. _spatialite_connect:
11+
1012
Connect to the DB
1113
-----------------
1214

1315
Just like when using PostGIS connecting to a SpatiaLite database requires an ``Engine``. This is how
1416
you create one for SpatiaLite::
1517

18+
>>> from geoalchemy2 import load_spatialite
1619
>>> from sqlalchemy import create_engine
1720
>>> from sqlalchemy.event import listen
1821
>>>
19-
>>> def load_spatialite(dbapi_conn, connection_record):
20-
... dbapi_conn.enable_load_extension(True)
21-
... dbapi_conn.load_extension('/usr/lib/x86_64-linux-gnu/mod_spatialite.so')
22-
...
23-
>>>
24-
>>> engine = create_engine('sqlite:///gis.db', echo=True)
25-
>>> listen(engine, 'connect', load_spatialite)
22+
>>> engine = create_engine("sqlite:///gis.db", echo=True)
23+
>>> listen(engine, "connect", load_spatialite)
2624

2725
The call to ``create_engine`` creates an engine bound to the database file ``gis.db``. After that
2826
a ``connect`` listener is registered on the engine. The listener is responsible for loading the
29-
SpatiaLite extension, which is a necessary operation for using SpatiaLite through SQL.
27+
SpatiaLite extension, which is a necessary operation for using SpatiaLite through SQL. The path to
28+
the ``mod_spatialite`` file should be stored in the ``SPATIALITE_LIBRARY_PATH`` environment
29+
variable before using the ``load_spatialite`` function.
3030

3131
At this point you can test that you are able to connect to the database::
3232

3333
>> conn = engine.connect()
34-
2018-05-30 17:12:02,675 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
35-
2018-05-30 17:12:02,676 INFO sqlalchemy.engine.base.Engine ()
36-
2018-05-30 17:12:02,676 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
37-
2018-05-30 17:12:02,676 INFO sqlalchemy.engine.base.Engine ()
38-
39-
You can also check that the ``gis.db`` SQLite database file was created on the file system.
40-
41-
One additional step is required for using SpatiaLite: create the ``geometry_columns`` and
42-
``spatial_ref_sys`` metadata tables. This is done by calling SpatiaLite's ``InitSpatialMetaData``
43-
function::
4434

45-
>>> from sqlalchemy.sql import select, func
46-
>>>
47-
>>> conn.execute(select([func.InitSpatialMetaData()]))
35+
Note that this call will internally call the ``load_spatialite`` function, which can take some time
36+
to execute on a new database because it actually calls the ``InitSpatialMetaData`` function from
37+
SpatiaLite.
38+
Then you can also check that the ``gis.db`` SQLite database file was created on the file system.
4839

49-
Note that this operation may take some time the first time it is executed for a database. When
50-
``InitSpatialMetaData`` is executed again it will report an error::
40+
Note that when ``InitSpatialMetaData`` is executed again it will report an error::
5141

5242
InitSpatiaMetaData() error:"table spatial_ref_sys already exists"
5343

@@ -61,9 +51,7 @@ Declare a Mapping
6151
-----------------
6252

6353
Now that we have a working connection we can go ahead and create a mapping between
64-
a Python class and a database table.
65-
66-
::
54+
a Python class and a database table::
6755

6856
>>> from sqlalchemy.ext.declarative import declarative_base
6957
>>> from sqlalchemy import Column, Integer, String
@@ -72,17 +60,14 @@ a Python class and a database table.
7260
>>> Base = declarative_base()
7361
>>>
7462
>>> class Lake(Base):
75-
... __tablename__ = 'lake'
63+
... __tablename__ = "lake"
7664
... id = Column(Integer, primary_key=True)
7765
... name = Column(String)
78-
... geom = Column(Geometry(geometry_type='POLYGON', management=True))
66+
... geom = Column(Geometry(geometry_type="POLYGON"))
7967

80-
This basically works in the way as with PostGIS. The difference is the ``management``
81-
argument that must be set to ``True``.
82-
83-
Setting ``management`` to ``True`` indicates that the ``AddGeometryColumn`` and
84-
``DiscardGeometryColumn`` management functions will be used for the creation and removal of the
85-
geometry column. This is required with SpatiaLite.
68+
From the user point of view this works in the same way as with PostGIS. The difference is that
69+
internally the ``RecoverGeometryColumn`` and ``DiscardGeometryColumn`` management functions will be
70+
used for the creation and removal of the geometry column.
8671

8772
Create the Table in the Database
8873
--------------------------------
@@ -117,25 +102,25 @@ do it using GeoAlchemy 2 with PostGIS.
117102

118103
::
119104

120-
>>> lake = Lake(name='Majeur', geom='POLYGON((0 0,1 0,1 1,0 1,0 0))')
105+
>>> lake = Lake(name="Majeur", geom="POLYGON((0 0,1 0,1 1,0 1,0 0))")
121106
>>> session.add(lake)
122107
>>> session.commit()
123108

124109
We can now query the database for ``Majeur``::
125110

126-
>>> our_lake = session.query(Lake).filter_by(name='Majeur').first()
111+
>>> our_lake = session.query(Lake).filter_by(name="Majeur").first()
127112
>>> our_lake.name
128-
u'Majeur'
113+
u"Majeur"
129114
>>> our_lake.geom
130-
<WKBElement at 0x9af594c; '0103000000010000000500000000000000000000000000000000000000000000000000f03f0000000000000000000000000000f03f000000000000f03f0000000000000000000000000000f03f00000000000000000000000000000000'>
115+
<WKBElement at 0x9af594c; "0103000000010000000500000000000000000000000000000000000000000000000000f03f0000000000000000000000000000f03f000000000000f03f0000000000000000000000000000f03f00000000000000000000000000000000">
131116
>>> our_lake.id
132117
1
133118

134119
Let's add more lakes::
135120

136121
>>> session.add_all([
137-
... Lake(name='Garde', geom='POLYGON((1 0,3 0,3 2,1 2,1 0))'),
138-
... Lake(name='Orta', geom='POLYGON((3 0,6 0,6 3,3 3,3 0))')
122+
... Lake(name="Garde", geom="POLYGON((1 0,3 0,3 2,1 2,1 0))"),
123+
... Lake(name="Orta", geom="POLYGON((3 0,6 0,6 3,3 3,3 0))")
139124
... ])
140125
>>> session.commit()
141126

@@ -156,7 +141,7 @@ Now a spatial query::
156141

157142
>>> from geolachemy2 import WKTElement
158143
>>> query = session.query(Lake).filter(
159-
... func.ST_Contains(Lake.geom, WKTElement('POINT(4 1)')))
144+
... func.ST_Contains(Lake.geom, WKTElement("POINT(4 1)")))
160145
...
161146
>>> for lake in query:
162147
... print(lake.name)
@@ -166,7 +151,7 @@ Now a spatial query::
166151
Here's another spatial query, using ``ST_Intersects`` this time::
167152

168153
>>> query = session.query(Lake).filter(
169-
... Lake.geom.ST_Intersects(WKTElement('LINESTRING(2 1,4 1)')))
154+
... Lake.geom.ST_Intersects(WKTElement("LINESTRING(2 1,4 1)")))
170155
...
171156
>>> for lake in query:
172157
... print(lake.name)
@@ -176,8 +161,8 @@ Here's another spatial query, using ``ST_Intersects`` this time::
176161

177162
We can also apply relationship functions to :class:`geoalchemy2.elements.WKBElement`. For example::
178163

179-
>>> lake = session.query(Lake).filter_by(name='Garde').one()
180-
>>> print(session.scalar(lake.geom.ST_Intersects(WKTElement('LINESTRING(2 1,4 1)'))))
164+
>>> lake = session.query(Lake).filter_by(name="Garde").one()
165+
>>> print(session.scalar(lake.geom.ST_Intersects(WKTElement("LINESTRING(2 1,4 1)"))))
181166
1
182167

183168
``session.scalar`` allows executing a clause and returning a scalar value (an integer value in this
@@ -191,22 +176,59 @@ Function mapping
191176

192177
Several functions have different names in SpatiaLite than in PostGIS. The GeoAlchemy 2 package is
193178
based on the PostGIS syntax but it is possible to automatically translate the queries into
194-
SpatiaLite ones. For example, the function `ST_GeomFromEWKT` is automatically translated into
195-
`GeomFromEWKT`. Unfortunately, only a few functions are automatically mapped (the ones internally
196-
used by GeoAlchemy 2). Nevertheless, it is possible to define new mappings in order to translate
197-
the queries automatically. Here is an example to register a mapping for the `ST_Buffer` function::
179+
SpatiaLite ones. For example, the function ``ST_GeomFromEWKT`` is automatically translated into
180+
``GeomFromEWKT``. Unfortunately, only a few functions are automatically mapped (mainly the ones
181+
internally used by GeoAlchemy 2). Nevertheless, it is possible to define new mappings in order to
182+
translate the queries automatically. Here is an example to register a mapping for the ``ST_Buffer``
183+
function::
198184

199185
>>> geoalchemy2.functions.register_sqlite_mapping(
200-
... {'ST_Buffer': 'Buffer'}
186+
... {"ST_Buffer": "Buffer"}
201187
... )
202188

203-
After this command, all `ST_Buffer` calls in the queries will be translated to `Buffer` calls when
204-
the query is executed on a SQLite DB.
189+
After this command, all ``ST_Buffer`` calls in the queries will be translated to ``Buffer`` calls
190+
when the query is executed on a SQLite DB.
191+
192+
A more complex example is provided for when the PostGIS function should be mapped depending on
193+
the given parameters. For example, the ``ST_Buffer`` function can actually be translate into either
194+
the ``Buffer`` function or the ``SingleSidedBuffer`` function (only when ``side=right`` or ``side=left``
195+
is passed). See the :ref:`sphx_glr_gallery_test_specific_compilation.py` example in the gallery.
196+
197+
GeoPackage format
198+
-----------------
199+
200+
Starting from the version ``4.2`` of Spatialite, it is possible to use GeoPackage files as DB
201+
containers. GeoAlchemy 2 is able to handle most of the GeoPackage features automatically if the
202+
GeoPackage dialect is used (i.e. the DB URL starts with ``gpkg:///``) and the SpatiaLite extension
203+
is loaded. Usually, this extension should be loaded using the ``load_spatialite_gpkg`` listener::
204+
205+
>>> from geoalchemy2 import load_spatialite_gpkg
206+
>>> from sqlalchemy import create_engine
207+
>>> from sqlalchemy.event import listen
208+
>>>
209+
>>> engine = create_engine("gpkg:///gis.gpkg", echo=True)
210+
>>> listen(engine, "connect", load_spatialite_gpkg)
211+
212+
When using the ``load_spatialite_gpkg`` listener on a DB recognized as a GeoPackage, specific
213+
processes are activated:
214+
215+
* the base tables are created if they are missing,
216+
* the ``Amphibious`` mode is enabled using the ``EnableGpkgAmphibiousMode`` function,
217+
* the ``VirtualGPKG`` wrapper is activated using the ``AutoGpkgStart`` function.
218+
219+
After that it should be possible to use a GeoPackage the same way as a standard SpatiaLite
220+
database. GeoAlchemy 2 should be able to handle the following features in a transparent way for the
221+
user:
222+
223+
* create/drop spatial tables,
224+
* automatically create/drop spatial indexes if required,
225+
* reflect spatial tables,
226+
* use spatial functions on inserted geometries.
227+
228+
.. Note::
205229

206-
A more complex example is provided for when the `PostGIS` function should be mapped depending on
207-
the given parameters. For example, the `ST_Buffer` function can actually be translate into either
208-
the `Buffer` function or the `SingleSidedBuffer` function (only when `side=right` or `side=left` is
209-
passed). See the :ref:`sphx_glr_gallery_test_specific_compilation.py` example in the gallery.
230+
If you want to use the ``ST_Transform`` function you should call the
231+
:func:`geoalchemy2.admin.dialects.geopackage.create_spatial_ref_sys_view` first.
210232

211233
Further Reference
212234
-----------------

geoalchemy2/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from geoalchemy2 import functions # noqa
66
from geoalchemy2 import shape # noqa
77
from geoalchemy2 import types # noqa
8+
from geoalchemy2.admin.dialects.geopackage import load_spatialite_gpkg # noqa
89
from geoalchemy2.admin.dialects.sqlite import load_spatialite # noqa
910
from geoalchemy2.elements import CompositeElement # noqa
1011
from geoalchemy2.elements import RasterElement # noqa
@@ -58,6 +59,7 @@
5859
"elements",
5960
"exc",
6061
"load_spatialite",
62+
"load_spatialite_gpkg",
6163
"shape",
6264
"types",
6365
]

geoalchemy2/admin/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
def select_dialect(dialect_name):
1818
"""Select the dialect from its name."""
1919
known_dialects = {
20+
"geopackage": dialects.geopackage,
2021
"mysql": dialects.mysql,
2122
"postgresql": dialects.postgresql,
2223
"sqlite": dialects.sqlite,
@@ -25,6 +26,8 @@ def select_dialect(dialect_name):
2526

2627

2728
def setup_ddl_event_listeners():
29+
"""Setup the DDL event listeners to automatically process spatial columns."""
30+
2831
@event.listens_for(Table, "before_create")
2932
def before_create(table, bind, **kw):
3033
"""Handle spatial indexes."""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""This module defines some dialect-specific functions used for administration tasks."""
22
from geoalchemy2.admin.dialects import common # noqa
3+
from geoalchemy2.admin.dialects import geopackage # noqa
34
from geoalchemy2.admin.dialects import mysql # noqa
45
from geoalchemy2.admin.dialects import postgresql # noqa
56
from geoalchemy2.admin.dialects import sqlite # noqa

geoalchemy2/admin/dialects/common.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ def reflect_geometry_column(inspector, table, column_info):
7979

8080

8181
def check_management(column, dialect_name):
82-
return getattr(column.type, "management", False) is True or dialect_name in ["sqlite", "mysql"]
82+
return getattr(column.type, "management", False) is True or dialect_name in [
83+
"geopackage",
84+
"sqlite",
85+
"mysql",
86+
]
8387

8488

8589
def before_create(table, bind, **kw):

0 commit comments

Comments
 (0)