Skip to content

Commit 1f3189e

Browse files
committed
Added support for geo_point and geo_shape to the SQLAlchemy dialect
This should address #273
1 parent a0a63b8 commit 1f3189e

File tree

9 files changed

+168
-2
lines changed

9 files changed

+168
-2
lines changed

CHANGES.txt

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
Changes for crate
33
=================
44

5+
Unreleased
6+
==========
7+
8+
- Added support for ``geo_point`` and ``geo_json`` types to the SQLAlchemy
9+
dialect.
10+
511
2020/05/27 0.24.0
612
=================
713

docs/appendices/data-types.rst

+4
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ CrateDB SQLAlchemy
127127
`array`__ `ARRAY`__
128128
`object`__ :ref:`object` |nbsp| (extension type)
129129
`array(object)`__ :ref:`objectarray` |nbsp| (extension type)
130+
`geo_point`__ :ref:`geopoint` |nbsp| (extension type)
131+
`geo_shape`__ :ref:`geoshape` |nbsp| (extension type)
130132
================= =========================================
131133

132134
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#boolean
@@ -151,6 +153,8 @@ __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#a
151153
__ http://docs.sqlalchemy.org/en/latest/core/type_basics.html#sqlalchemy.types.ARRAY
152154
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#object
153155
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#array
156+
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-point
157+
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-shape
154158

155159
.. _json: https://docs.python.org/3/library/json.html
156160
.. _HTTP endpoint: https://crate.io/docs/crate/reference/en/latest/interfaces/http.html

docs/sqlalchemy.rst

+54
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,59 @@ The resulting object will look like this::
284284
change, the `UPDATE`_ statement sent to CrateDB will include all of the
285285
``ObjectArray`` data.
286286

287+
.. _geopoint:
288+
.. _geoshape:
289+
290+
``Geopoint`` and ``Geoshape``
291+
.............................
292+
293+
The CrateDB SQLAlchemy dialect provides two geospatial types:
294+
295+
- ``Geopoint``, which represents a longitude and latitude coordinate
296+
- ``Geoshape``, which is used to store geometric `GeoJSON geometry objects`_
297+
298+
To use these types, you can create columns, like so::
299+
300+
>>> class City(Base):
301+
...
302+
... __tablename__ = 'cities'
303+
... name = sa.Column(sa.String, primary_key=True)
304+
... coordinate = sa.Column(types.Geopoint)
305+
... area = sa.Column(types.Geoshape)
306+
307+
There are multiple ways of creating a geopoint. Firstly, you can define it as
308+
a tuple of ``(longitude, latitude)``::
309+
310+
>>> point = (139.76, 35.68)
311+
312+
Secondly, you can define it as a geojson ``Point`` object::
313+
314+
>>> from geojson import Point
315+
>>> point = Point(coordinates=(139.76, 35.68))
316+
317+
To create a geoshape, you can use a geojson shape object, such as a ``Polygon``::
318+
319+
>>> from geojson import Point, Polygon
320+
>>> area = Polygon(
321+
... [
322+
... [
323+
... (139.806, 35.515),
324+
... (139.919, 35.703),
325+
... (139.768, 35.817),
326+
... (139.575, 35.760),
327+
... (139.584, 35.619),
328+
... (139.806, 35.515),
329+
... ]
330+
... ]
331+
... )
332+
333+
You can then set the values of the ``Geopoint`` and ``Geoshape`` columns::
334+
335+
>>> tokyo = City(name="Tokyo", coordinate=point, area=area)
336+
>>> session.add(tokyo)
337+
>>> session.commit()
338+
339+
287340
Querying
288341
========
289342

@@ -535,3 +588,4 @@ column on the ``Character`` class.
535588
.. _score: https://crate.io/docs/crate/reference/en/latest/general/dql/fulltext.html#usage
536589
.. _working with tables: http://docs.sqlalchemy.org/en/latest/core/metadata.html
537590
.. _UUIDs: https://docs.python.org/3/library/uuid.html
591+
.. _geojson geometry objects: https://tools.ietf.org/html/rfc7946#section-3.1

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def read(path):
6969
extras_require=dict(
7070
test=['zope.testing',
7171
'zc.customdoctests>=1.0.1'],
72-
sqlalchemy=['sqlalchemy>=1.0,<1.4']
72+
sqlalchemy=['sqlalchemy>=1.0,<1.4', 'geojson>=2.5.0']
7373
),
7474
python_requires='>=3.4',
7575
install_requires=requirements,

src/crate/client/doctests/sqlalchemy.txt

+47
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,53 @@ all values of that field of all objects in that object array::
182182
>>> query.all()
183183
[([1, 2, 3],), (None,)]
184184

185+
186+
Geospatial Types
187+
================
188+
189+
Geospatial types, such as ``geo_point`` and ``geo_area`` can also be used as
190+
part of a sqlalchemy schema::
191+
192+
>>> from crate.client.sqlalchemy.types import Geopoint, Geoshape
193+
194+
>>> class City(Base):
195+
... __tablename__ = 'cities'
196+
... name = sa.Column(sa.String, primary_key=True)
197+
... coordinate = sa.Column(Geopoint)
198+
... area = sa.Column(Geoshape)
199+
200+
One way of inserting these types is using the Geojson library, to create
201+
points or shapes::
202+
203+
>>> from geojson import Point, Polygon
204+
>>> area = Polygon(
205+
... [
206+
... [
207+
... (139.806, 35.515),
208+
... (139.919, 35.703),
209+
... (139.768, 35.817),
210+
... (139.575, 35.760),
211+
... (139.584, 35.619),
212+
... (139.806, 35.515),
213+
... ]
214+
... ]
215+
... )
216+
>>> point = Point(coordinates=(139.76, 35.68))
217+
218+
These two objects can then be added to an sqlalchemy model and added to the
219+
session::
220+
221+
>>> tokyo = City(coordinate=point, area=area, name='Tokyo')
222+
>>> session.add(tokyo)
223+
>>> session.commit()
224+
225+
When retrieved, they are retrieved as the corresponding geojson objects::
226+
227+
>>> refresh("cities")
228+
>>> query = session.query(City.name, City.coordinate, City.area)
229+
>>> query.all()
230+
[('Tokyo', (139.76, 35.68), {"coordinates": [[[139.806, 35.515], [139.919, 35.703], [139.768, 35.817], [139.575, 35.76], [139.584, 35.619], [139.806, 35.515]]], "type": "Polygon"})]
231+
185232
Count and Group By
186233
==================
187234

src/crate/client/sqlalchemy/doctests/reflection.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ List all schemas::
1212
List all tables::
1313

1414
>>> inspector.get_table_names()
15-
['characters', 'locations']
15+
['characters', 'cities', 'locations']
1616

1717
>>> set(['checks', 'cluster', 'jobs', 'jobs_log']).issubset(inspector.get_table_names(schema='sys'))
1818
True

src/crate/client/sqlalchemy/types.py

+45
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from sqlalchemy.sql import default_comparator
2525
from sqlalchemy.ext.mutable import Mutable
2626

27+
import geojson
28+
2729

2830
class MutableList(Mutable, list):
2931

@@ -217,3 +219,46 @@ def get_col_spec(self, **kws):
217219

218220

219221
ObjectArray = MutableList.as_mutable(_ObjectArray)
222+
223+
224+
class Geopoint(sqltypes.UserDefinedType):
225+
226+
class Comparator(sqltypes.TypeEngine.Comparator):
227+
228+
def __getitem__(self, key):
229+
return default_comparator._binary_operate(self.expr,
230+
operators.getitem,
231+
key)
232+
233+
def get_col_spec(self):
234+
return 'GEO_POINT'
235+
236+
def bind_processor(self, dialect):
237+
def process(value):
238+
if isinstance(value, geojson.Point):
239+
return value.coordinates
240+
return value
241+
return process
242+
243+
def result_processor(self, dialect, coltype):
244+
return tuple
245+
246+
comparator_factory = Comparator
247+
248+
249+
class Geoshape(sqltypes.UserDefinedType):
250+
251+
class Comparator(sqltypes.TypeEngine.Comparator):
252+
253+
def __getitem__(self, key):
254+
return default_comparator._binary_operate(self.expr,
255+
operators.getitem,
256+
key)
257+
258+
def get_col_spec(self):
259+
return 'GEO_SHAPE'
260+
261+
def result_processor(self, dialect, coltype):
262+
return geojson.GeoJSON.to_instance
263+
264+
comparator_factory = Comparator

src/crate/client/tests.py

+9
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,14 @@ def setUpCrateLayerAndSqlAlchemy(test):
174174
INDEX quote_ft using fulltext(quote) with (analyzer = 'english')
175175
) """)
176176

177+
with connect(crate_host) as conn:
178+
cursor = conn.cursor()
179+
cursor.execute("""create table cities (
180+
name string primary key,
181+
coordinate geo_point,
182+
area geo_shape
183+
) """)
184+
177185
engine = sa.create_engine('crate://{0}'.format(crate_host))
178186
Base = declarative_base()
179187

@@ -279,6 +287,7 @@ def tearDownWithCrateLayer(test):
279287
for stmt in ["DROP TABLE locations",
280288
"DROP BLOB TABLE myfiles",
281289
"DROP TABLE characters",
290+
"DROP TABLE cities",
282291
"DROP USER me",
283292
"DROP USER trusted_me",
284293
]:

versions.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ MarkupSafe = 0.23
1010
Pygments = 1.6
1111
Sphinx = 1.2.3
1212
SQLAlchemy = 1.3.17
13+
geojson = 2.5.0
1314
coverage = 5.0.3
1415
crate-docs-theme = 0.5.0
1516
createcoverage = 1.5

0 commit comments

Comments
 (0)