#28702: Field.db_type, Field.get_internal_type() and postgresql ::citext
----------------------------------+--------------------------------------
Reporter: Дилян Палаузов | Owner: (none)
Type: Uncategorized | Status: new
Component: contrib.postgres | Version: 1.11
Severity: Normal | Resolution:
Keywords: | Triage Stage: Unreviewed
Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
----------------------------------+--------------------------------------
Old description:
> According to https://docs.djangoproject.com/en/1.11/howto/custom-model-
> fields/#custom-database-types db_type() is used when constructing a WHERE
> clause that includes the model field – that is, when you retrieve data
> using QuerySet methods like get(), filter(), and exclude() and have the
> model field as an argument.
>
> Then https://docs.djangoproject.com/en/1.11/howto/custom-model-fields
> /#emulating-built-in-field-types states, that get_internal_type() is used
> for correct usage of columns and during migrations.
>
> This would imply that having a
> django.contrib.postgresq.fields.CICharField field, calling
> Model.objects.filter(field_contains="ABC") would be converted to 'WHERE
> field::citext LIKE "%ABC%"' but it is converted to 'WHERE field::text
> LIKE "%ABC%"', which does not work as expected, so one has to call
> filter(field_icontains="ABC") instead. The reason is that
> django/db/models/lookups.py:BuiltinLookup.process_lhs() passes the result
> of get_internal_type to
> db/backends/postgresql/operations.py:lookup_cast() and this is used to
> distinguish the right lookup.
>
> My proposal for fixing the code for ::citext:
> {{{
> diff --git a/django/contrib/postgres/fields/citext.py
> b/django/contrib/postgres/fields/citext.py
> --- a/django/contrib/postgres/fields/citext.py
> +++ b/django/contrib/postgres/fields/citext.py
> @@ -7,6 +7,9 @@ class CIText:
> def db_type(self, connection):
> return 'citext'
>
> + def get_internal_type(self):
> + return "CI" + super().get_internal_type()
> +
>
> class CICharField(CIText, CharField):
> pass
> diff --git a/django/db/backends/postgresql/operations.py
> b/django/db/backends/postgresql/operations.py
> --- a/django/db/backends/postgresql/operations.py
> +++ b/django/db/backends/postgresql/operations.py
> @@ -77,6 +77,8 @@ class DatabaseOperations(BaseDatabaseOperations):
> 'istartswith', 'endswith', 'iendswith',
> 'regex', 'iregex'):
> if internal_type in ('IPAddressField',
> 'GenericIPAddressField'):
> lookup = "HOST(%s)"
> + elif internal_type in ('CICharField', 'CITextField'):
> + lookup = "%s::citext"
> else:
> lookup = "%s::text"
>
> diff --git a/django/db/backends/postgresql/schema.py
> b/django/db/backends/postgresql/schema.py
> --- a/django/db/backends/postgresql/schema.py
> +++ b/django/db/backends/postgresql/schema.py
> @@ -112,6 +112,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
> if (old_field.db_index or old_field.unique) and (
> (old_type.startswith('varchar') and not
> new_type.startswith('varchar')) or
> (old_type.startswith('text') and not
> new_type.startswith('text'))
> + (old_type.startswith('citext') and not
> new_type.startswith('citext'))
> ):
> index_name = self._create_index_name(model._meta.db_table,
> [old_field.column], suffix='_like')
> self.execute(self._delete_constraint_sql(self.sql_delete_index, model,
> index_name))
> @@ -131,3 +132,11 @@ class
> DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
> if old_field.unique and not (new_field.db_index or
> new_field.unique):
> index_to_remove =
> self._create_index_name(model._meta.db_table, [old_field.column],
> suffix='_like')
> self.execute(self._delete_constraint_sql(self.sql_delete_index, model,
> index_to_remove))
> +
> +
> + def _alter_column_null_sql(self, model, old_field, new_field):
> + if (self.connection.features.interprets_empty_strings_as_nulls
> and
> + new_field.get_internal_type() in ("CICharField",
> "CITextField")):
> + # The field is nullable in the database anyway, leave it
> alone.
> + return
> + return super()._alter_column_null_sql(model, old_field,
> new_field
> }}}
New description:
According to https://docs.djangoproject.com/en/1.11/howto/custom-model-
fields/#custom-database-types db_type() is used when constructing a WHERE
clause that includes the model field – that is, when you retrieve data
using QuerySet methods like get(), filter(), and exclude() and have the
model field as an argument.
Then https://docs.djangoproject.com/en/1.11/howto/custom-model-fields
/#emulating-built-in-field-types states, that get_internal_type() is used
for correct usage of columns and during migrations.
This would imply that having a django.contrib.postgresq.fields.CICharField
field, calling Model.objects.filter(field_contains="ABC") would be
converted to 'WHERE field::citext LIKE "%ABC%"' but it is converted to
'WHERE field::text LIKE "%ABC%"', which does not work as expected, so one
has to call filter(field_icontains="ABC") instead. The reason is that
django/db/models/lookups.py:BuiltinLookup.process_lhs() passes the result
of get_internal_type to
db/backends/postgresql/operations.py:lookup_cast() and this is used to
distinguish the right lookup.
My proposal for fixing the code for ::citext:
{{{
diff --git a/django/contrib/postgres/fields/citext.py
b/django/contrib/postgres/fields/citext.py
--- a/django/contrib/postgres/fields/citext.py
+++ b/django/contrib/postgres/fields/citext.py
@@ -7,6 +7,9 @@ class CIText:
def db_type(self, connection):
return 'citext'
+ def get_internal_type(self):
+ return "CI" + super().get_internal_type()
+
class CICharField(CIText, CharField):
pass
diff --git a/django/db/backends/postgresql/operations.py
b/django/db/backends/postgresql/operations.py
--- a/django/db/backends/postgresql/operations.py
+++ b/django/db/backends/postgresql/operations.py
@@ -77,6 +77,8 @@ class DatabaseOperations(BaseDatabaseOperations):
'istartswith', 'endswith', 'iendswith',
'regex', 'iregex'):
if internal_type in ('IPAddressField',
'GenericIPAddressField'):
lookup = "HOST(%s)"
+ elif internal_type in ('CICharField', 'CITextField'):
+ lookup = "%s::citext"
else:
lookup = "%s::text"
diff --git a/django/db/backends/postgresql/schema.py
b/django/db/backends/postgresql/schema.py
--- a/django/db/backends/postgresql/schema.py
+++ b/django/db/backends/postgresql/schema.py
@@ -112,6 +112,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
if (old_field.db_index or old_field.unique) and (
(old_type.startswith('varchar') and not
new_type.startswith('varchar')) or
(old_type.startswith('text') and not
new_type.startswith('text'))
+ (old_type.startswith('citext') and not
new_type.startswith('citext'))
):
index_name = self._create_index_name(model._meta.db_table,
[old_field.column], suffix='_like')
self.execute(self._delete_constraint_sql(self.sql_delete_index, model,
index_name))
}}}
--
Comment (by Дилян Палаузов):
I agree that overriding _alter_column_null_sql() makes no sense and have
removed it from the description.
--
Ticket URL: <https://code.djangoproject.com/ticket/28702#comment:2>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/072.15d144da2a68d6ae8107b0e200ee277f%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.