Skip to content

Commit

Permalink
Fix: Can handle negative coordinates with Spatialite (#517)
Browse files Browse the repository at this point in the history
* Fix: Can handle negative coordinates with Spatialite

* Feat(spatialite): Add warning for non-parsable WKT
  • Loading branch information
adrien-berchet authored Jul 10, 2024
1 parent 8b7a2b1 commit f32331b
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 6 deletions.
2 changes: 1 addition & 1 deletion geoalchemy2/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class WKTElement(_SpatialElement):
"""

_REMOVE_SRID = re.compile("(SRID=([0-9]+); ?)?(.*)")
SPLIT_WKT_PATTERN = re.compile(r"((SRID=\d+) *; *)?([\w ]+) *(\([\d\. ,\(\)]+\))")
SPLIT_WKT_PATTERN = re.compile(r"((SRID=\d+) *; *)?([\w ]+) *(\([-\d\. ,\(\)]+\))")

geom_from: str = "ST_GeomFromText"
geom_from_extended_version: str = "ST_GeomFromEWKT"
Expand Down
5 changes: 5 additions & 0 deletions geoalchemy2/types/dialects/sqlite.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""This module defines specific functions for SQLite dialect."""

import re
import warnings

from geoalchemy2.elements import RasterElement
from geoalchemy2.elements import WKBElement
Expand All @@ -12,6 +13,10 @@ def format_geom_type(wkt, default_srid=None):
"""Format the Geometry type for SQLite."""
match = re.match(WKTElement.SPLIT_WKT_PATTERN, wkt)
if match is None:
warnings.warn(
"The given WKT could not be parsed by GeoAlchemy2, this could lead to undefined "
f"behavior with Z, M or ZM geometries or with incorrect SRID. The WKT string is: {wkt}"
)
return wkt
_, srid, geom_type, coords = match.groups()
geom_type = geom_type.replace(" ", "")
Expand Down
9 changes: 4 additions & 5 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import sys

import pytest
from packaging import version
from pkg_resources import parse_version
from packaging.version import parse as parse_version
from sqlalchemy import __version__ as SA_VERSION
from sqlalchemy import create_engine
from sqlalchemy import select as raw_select
Expand All @@ -30,9 +29,9 @@ def __call__(self, test_obj):

def get_postgis_major_version(bind):
try:
return version.parse(bind.execute(func.postgis_lib_version()).scalar()).major
return parse_version(bind.execute(func.postgis_lib_version()).scalar()).major
except OperationalError:
return version.parse("0").major
return parse_version("0").major


def get_postgres_major_version(bind):
Expand Down Expand Up @@ -81,7 +80,7 @@ def skip_pypy(msg=None):


def select(args):
if version.parse(SA_VERSION) < version.parse("1.4"):
if parse_version(SA_VERSION) < parse_version("1.4"):
return raw_select(args)
else:
return raw_select(*args)
Expand Down
26 changes: 26 additions & 0 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,32 @@ def test_insert_geom_poi(self, conn, Poi, setup_tables):
assert srid == 4326
assert row[1] == from_shape(Point(1, 1), srid=4326, extended=True)

def test_insert_negative_coords(self, conn, Poi, setup_tables, dialect_name):
conn.execute(
Poi.__table__.insert(),
[
{"geom": "SRID=4326;POINT(-1 1)"},
{"geom": WKTElement("POINT(-1 1)", srid=4326)},
{"geom": WKTElement("SRID=4326;POINT(-1 1)", extended=True)},
{"geom": from_shape(Point(-1, 1), srid=4326)},
{"geom": from_shape(Point(-1, 1), srid=4326, extended=True)},
],
)

results = conn.execute(Poi.__table__.select())
rows = results.fetchall()

for row in rows:
assert isinstance(row[1], WKBElement)
wkt = conn.execute(row[1].ST_AsText()).scalar()
assert format_wkt(wkt) == "POINT(-1 1)"
srid = conn.execute(row[1].ST_SRID()).scalar()
assert srid == 4326
if dialect_name == "mysql":
assert row[1] == from_shape(Point(-1, 1), srid=4326)
else:
assert row[1] == from_shape(Point(-1, 1), srid=4326, extended=True)


class TestSelectBindParam:
@pytest.fixture
Expand Down
48 changes: 48 additions & 0 deletions tests/test_functional_sqlite.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import re

import pytest
from shapely.geometry import GeometryCollection
from shapely.geometry import LineString
from shapely.geometry import Point
from sqlalchemy import CheckConstraint
from sqlalchemy import Column
from sqlalchemy import Integer
Expand All @@ -22,6 +24,7 @@
from geoalchemy2.shape import from_shape
from geoalchemy2.shape import to_shape

from . import format_wkt
from . import select
from . import skip_case_insensitivity
from . import skip_pypy
Expand Down Expand Up @@ -310,6 +313,51 @@ def test_load_spatialite_no_env_variable(self, monkeypatch, conn):
load_spatialite(conn.connection.dbapi_connection)


class TestInsertionCore:
@pytest.fixture
def GeomObject(self, base):
class GeomObject(base):
__tablename__ = "any_geom_object"
id = Column(Integer, primary_key=True)
geom = Column(Geometry(srid=4326))

return GeomObject

def test_insert_unparsable_WKT(self, conn, GeomObject, setup_tables, dialect_name):
with pytest.warns(
UserWarning,
match=(
"The given WKT could not be parsed by GeoAlchemy2, this could lead to undefined "
"behavior"
),
):
conn.execute(
GeomObject.__table__.insert(),
[
{"geom": "SRID=4326;GeometryCollection(POINT (-1 1),LINESTRING (2 2, 3 3))"},
],
)

results = conn.execute(GeomObject.__table__.select())
rows = results.fetchall()

for row in rows:
assert isinstance(row[1], WKBElement)
wkt = conn.execute(row[1].ST_AsText()).scalar()
assert format_wkt(wkt) == "GEOMETRYCOLLECTION(POINT(-1 1),LINESTRING(2 2,3 3))"
srid = conn.execute(row[1].ST_SRID()).scalar()
assert srid == 4326
if dialect_name == "mysql":
extended = None
else:
extended = True
assert row[1] == from_shape(
GeometryCollection([Point(-1, 1), LineString([[2, 2], [3, 3]])]),
srid=4326,
extended=extended,
)


class TestInsertionORM:
@pytest.fixture
def LocalPoint(self, base):
Expand Down

0 comments on commit f32331b

Please sign in to comment.