#27985: Converting `Foo.objects.filter(bar=None)` to an `IsNull` too early.
-------------------------------------+-------------------------------------
     Reporter:  Jarek Glowacki       |                    Owner:  (none)
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  master
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:  sql None NULL        |             Triage Stage:  Accepted
  transform                          |
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Changes (by Jarek Glowacki):

 * owner:  Jarek Glowacki => (none)
 * status:  assigned => new


Comment:

 Yeahh, nope.
 The current workflow is too convoluted to just slot in a patch for this.

 I couldn't find a way to untangle the ordering such that it'd cover this
 use case without breaking multiple other use cases.
 I'd suggest taking this ticket under consideration when someone comes
 round to refactor this file.

 Specifically, the functions `build_filter` and `prepare_lookup_value` need
 to be broken down into more managable bits. It's evident they've had extra
 conditional steps shoehorned in over time, which has made them very
 difficult to work with.


 For anyone who comes across this in the future, all we need to address
 this case is ensure that this snippet of code from `prepare_lookup_value`:
 {{{
         # Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject
 all
         # uses of None as a query value.
         if value is None:
             if lookups[-1] not in ('exact', 'iexact'):
                 raise ValueError("Cannot use None as a query value")
             return True, ['isnull'], used_joins
 }}}
 is evaluated AFTER the field's `get_prep_value` has processed the value.
 Here are the failing tests again:
 [https://github.com/django/django/compare/master...jarekwg:ticket_27985
 Failing tests]

 And here's a bandaid workaround that hacks two lookups and the form field
 to get around the problem.
 {{{
 class NulledCharField(models.CharField):
     """
     This is required when we want uniqueness on a CharField whilst also
 allowing it to be empty.
     In the DB, '' == '', but NULL != NULL. So we store emptystring as NULL
 in the db, but treat it
     as emptystring in the code.
     """
     description = _("CharField that stores emptystring as NULL but returns
 ''.")

     def from_db_value(self, value, expression, connection, context):
         if value is None:
             return ''
         else:
             return value

     def to_python(self, value):
         if isinstance(value, models.CharField):
             return value
         if value is None:
             return ''
         return value

     def get_prep_value(self, value):
         if value is '':
             return None
         else:
             return value

     class NulledCharField(forms.CharField):
         """ A form field for the encompassing model field. """
         def clean(self, value):
             if value == '':
                 return None
             return value

     def formfield(self, form_class=None, **kwargs):
         return
 super().formfield(form_class=self.__class__.NulledCharField, **kwargs)


 @NulledCharField.register_lookup
 class NulledCharExactLookup(Exact):
     """ Generate ISNULL in the `exact` lookup, because Django won't use
 the `isnull` lookup correctly. """
     lookup_name = 'exact'

     def as_sql(self, compiler, connection):
         sql, params = compiler.compile(self.lhs)
         if self.rhs in ('', None):
             return "%s IS NULL" % sql, params
         else:
             return super().as_sql(compiler, connection)


 @NulledCharField.register_lookup
 class NulledCharIsNullLookup(IsNull):
     """
     Doing something like Foo.objects.exclude(bar='') has django trying to
 shove
     an extra isnull check where it doesn't need one. We solve this by just
 crippling
     this lookup altogether. We don't need it as all ISNULL checks that we
 actually
     do want, we generate in the `exact` lookup. :(
     """
     lookup_name = 'isnull'
     prepare_rhs = False

     def as_sql(self, compiler, connection):
         sql, params = compiler.compile(self.lhs)
         return "TRUE", params
 }}}

--
Ticket URL: <https://code.djangoproject.com/ticket/27985#comment:4>
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/065.f6caf9846447e41d9283e125c544987b%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to