Opened 4 years ago
Closed 4 years ago
#33047 closed Bug (fixed)
CheckConstraint crashes with GIS lookup and PostGIS, MySQL, and Oracle backends.
| Reported by: | Daniel Swain | Owned by: | Claude Paroz |
|---|---|---|---|
| Component: | GIS | Version: | dev |
| Severity: | Normal | Keywords: | constraint, postgres, postgis, geos, attributeerror, migration, schema_editor, getquoted, quote_value |
| Cc: | Triage Stage: | Ready for checkin | |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Trying to apply a migration adding a CheckConstraint with a spatial lookup (within) fails with: AttributeError: 'str' object has no attribute 'decode'. when creating the SQL statement.
Traceback:
File "/usr/local/lib/python3.9/site-packages/django/db/migrations/operations/models.py", line 858, in database_forwards
schema_editor.add_constraint(model, self.constraint)
File "/usr/local/lib/python3.9/site-packages/django/db/backends/base/schema.py", line 379, in add_constraint
sql = constraint.create_sql(model, self)
File "/usr/local/lib/python3.9/site-packages/django/db/models/constraints.py", line 54, in create_sql
check = self._get_check_sql(model, schema_editor)
File "/usr/local/lib/python3.9/site-packages/django/db/models/constraints.py", line 47, in _get_check_sql
return sql % tuple(schema_editor.quote_value(p) for p in params)
File "/usr/local/lib/python3.9/site-packages/django/db/models/constraints.py", line 47, in <genexpr>
return sql % tuple(schema_editor.quote_value(p) for p in params)
File "/usr/local/lib/python3.9/site-packages/django/db/backends/postgresql/schema.py", line 45, in quote_value
return adapted.getquoted().decode()
Investigation
Looking at the DatabaseSchemaEditor class in the postgresql backend, it looks like quote_value expects adapted.getquoted() to return a bytestring:
def quote_value(self, value): ... # getquoted() returns a quoted bytestring of the adapted value. return adapted.getquoted().decode()
The adaper used in this migration, PostGISAdapter returns a str from getquoted, not a bytestring:
class PostGISAdapter: def getquoted(self): """ Return a properly quoted string for use in PostgreSQL/PostGIS. """ if self.is_geometry: # Psycopg will figure out whether to use E'\\000' or '\000'. return '%s(%s)' % ( 'ST_GeogFromWKB' if self.geography else 'ST_GeomFromEWKB', self._adapter.getquoted().decode() ) else: # For rasters, add explicit type cast to WKB string. return "'%s'::raster" % self.ewkb
Example model.
We have a model definition similar to:
from django.contrib.gis.db import models class Example(models.Model): location = models.models.PointField(null=True, blank=True)
We are trying to add a CheckConstraint that checks that the location is within a bounding box: (x0, y0, x1, y1) (The rest of these examples will use a bounding box for Australia).
from django.contrib.gis.geos import Polygon class Example(models.Model): location = models.models.PointField(null=True, blank=True) class Meta: constraints = [ models.CheckConstraint( check=models.Q(location__within=Polygon.from_bbox((96.816941, -43.740510, 167.998035, -9.142176)), name="location_in_australia", ), ]
A migration is successfully created:
class Migration(migration.Migration): operations = [ migrations.AddConstraint( model_name='example', constraint=models.CheckConstraint(check=models.Q(('location__within', django.contrib.gis.geos.polygon.Polygon(((96.816941, -43.74051), (96.816941, -9.142176), (167.998035, -9.142176), (167.998035, -43.74051), (96.816941, -43.74051))))), name='location_in_australia'), ), ]
However, trying to run that migration gives the traceback shown at the top.
Potential fix/workaround.
We were able to run the migration by editing the file django/contrib/gis/db/backends/postgis/adapter.py to make PostGISAdapter.getquoted() return a bytestring, but we don't know what effect this will have elsewhere.
Example:
def getquoted(self): if self.is_geometry: # Psycopg will figure out whether to use E'\\000' or '\000'. quoted = '%s(%s)' % ( 'ST_GeogFromWKB' if self.geography else 'ST_GeomFromEWKB', self._adapter.getquoted().decode() ) else: # For rasters, add explicit type cast to WKB string. quoted = "'%s'::raster" % self.ewkb return quoted.encode()
Change History (8)
comment:1 by , 4 years ago
| Component: | contrib.postgres → GIS |
|---|---|
| Owner: | set to |
| Summary: | str decode error when applying migration with a CheckConstraint using a GEOS geometry and PostGISAdapter → CheckConstraint crashes with GIS lookup and PostGIS backend. |
| Triage Stage: | Unreviewed → Accepted |
comment:2 by , 4 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
Hi,
I'm working on a fix, the topic branch : https://github.com/ArsaCode/django/tree/ticket_33047
comment:3 by , 4 years ago
PR (work in progress) : https://github.com/django/django/pull/15130
With current changes the build fails on mysql_gis :
database=mysql_gis,label=mariadb,python=python3.10
Error :
MySQLdb._exceptions.OperationalError: (1583, "Incorrect parameters in the call to native function 'ST_GeomFromText'")
database=mysql_gis,label=bionic-pr,python=python3.10
database=mysql_gis,label=bionic-pr,python=python3.8
database=mysql_gis,label=bionic-pr,python=python3.9
Error :
MySQLdb._exceptions.OperationalError: (1583, "Incorrect parameters in the call to native function 'st_geometryfromtext'")
It seems like the parameter given to the function "ST_GeomFromText"/"st_geometryfromtext" is invalid. I looked at the Polygon assertions in the MySQL documentation and the values I've put in the regression test migration seems not to be a valid Polygon for MySQL :
Polygon Assertions
The boundary of a Polygon consists of a set of LinearRing objects (that is, LineString objects that are both simple and closed) that make up its exterior and interior boundaries.
A Polygon has no rings that cross. The rings in the boundary of a Polygon may intersect at a Point, but only as a tangent.
A Polygon has no lines, spikes, or punctures.
A Polygon has an interior that is a connected point set.
A Polygon may have holes. The exterior of a Polygon with holes is not connected. Each hole defines a connected component of the exterior.
I think I'm not testing the patch correctly but I'm not sure how to do a regression test for it (since adding the new test migration was making the test fail before the patch). Any help would be appreciated :)
comment:5 by , 4 years ago
| Patch needs improvement: | set |
|---|---|
| Summary: | CheckConstraint crashes with GIS lookup and PostGIS backend. → CheckConstraint crashes with GIS lookup and PostGIS, MySQL, and Oracle backends. |
comment:6 by , 4 years ago
| Owner: | changed from to |
|---|
comment:7 by , 4 years ago
| Patch needs improvement: | unset |
|---|---|
| Triage Stage: | Accepted → Ready for checkin |
Thanks for the report.
Reproduced at 022d29c934107c515dd6d3181945146a2077bdf0.