﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
33047	CheckConstraint crashes with GIS lookup and PostGIS, MySQL, and Oracle backends.	Daniel Swain	Claude Paroz	"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`:

{{{#!python
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`:

{{{#!python
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:

{{{#!python
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).

{{{#!python
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:

{{{#!python
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:
{{{#!python
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()
}}}"	Bug	closed	GIS	dev	Normal	fixed	constraint, postgres, postgis, geos, attributeerror, migration, schema_editor, getquoted, quote_value		Ready for checkin	1	0	0	0	0	0
