#34254: Exists annotations can return non-boolean results (i.e. None) if used 
with
an empty QuerySet.
-------------------------------------+-------------------------------------
               Reporter:  Keryn      |          Owner:  nobody
  Knight                             |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  dev
  layer (models, ORM)                |       Keywords:  Exists
               Severity:  Normal     |  EmptyQuerySet sqlite postgres
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 I suspect this is following on from, but potentially separate to #33018 --
 because that ticket starts out using `Django 3.2` to observe that an empty
 queryset (`EmptyQuerySet` or whatever, via `none()`) can short circuit
 evaluation to be `0 as ...` in the SQL query, which is exactly the same
 problem I observed.

 However, as far as I can tell, the result of an `Exists(queryset.none())`
 can still return values outside of `True/False`, namely, **None**.

 Using Django `main` as of `4593bc5da115f2e808a803a4ec24104b6c7a6152` (from
 `Wed Jan 11 ... 2023`), here's the behaviour on both postgres and sqlite.
 In both scenarios I'm using  `3.10.2` with `psycopg2==2.9.3` and
 `sqlite3.sqlite_version` is `3.37.0` and `sqlite3.version` is `2.6.0`.
 IPython outputs **8** and **11** are the problems.

 {{{
 class A(models.Model):
     pass

 class B(models.Model):
     pass
 }}}

 {{{
 In [1]: from app.models import A, B

 In [2]: A.objects.using("pg").create()
 Out[2]: <A: A object (1)>

 In [3]: B.objects.using("pg").create()
 Out[3]: <B: B object (1)>

 In [4]: A.objects.using("sqlite").create()
 Out[4]: <A: A object (1)>

 In [4]: B.objects.using("sqlite").create()
 Out[4]: <B: B object (1)>

 In [5]: from django.db.models import Exists

 In [6]:
 
A.objects.using("sqlite").annotate(should_be_bool=Exists(B.objects.all())).first().should_be_bool
 Out[6]: True

 In [7]:
 
A.objects.using("sqlite").annotate(should_be_bool=Exists(B.objects.filter(pk=99999999))).first().should_be_bool
 Out[7]: False

 In [8]:
 
A.objects.using("sqlite").annotate(should_be_bool=Exists(B.objects.none())).first().should_be_bool
 # This is the problem, it returned neither True nor False

 In [9]:
 
A.objects.using("pg").annotate(should_be_bool=Exists(B.objects.all())).first().should_be_bool
 Out[9]: True

 In [10]:
 
A.objects.using("pg").annotate(should_be_bool=Exists(B.objects.filter(pk=99999999))).first().should_be_bool
 Out[10]: False

 In [11]:
 
A.objects.using("pg").annotate(should_be_bool=Exists(B.objects.none())).first().should_be_bool
 # This is the problem, it returned neither True nor False
 }}}
 And the queries, which are the same for postgres & sqlite:
 {{{
 # ...
 {'sql': 'SELECT "app_a"."id", EXISTS(SELECT 1 AS "a" FROM "app_b" LIMIT 1)
 AS "should_be_bool" FROM "app_a" ORDER BY "app_a"."id" ASC LIMIT 1',
   'time': '0.001'},
  {'sql': 'SELECT "app_a"."id", EXISTS(SELECT 1 AS "a" FROM "app_b" U0
 WHERE U0."id" = 99999999 LIMIT 1) AS "should_be_bool" FROM "app_a" ORDER
 BY "app_a"."id" ASC LIMIT 1',
   'time': '0.001'},
  {'sql': 'SELECT "app_a"."id", NULL AS "should_be_bool" FROM "app_a" ORDER
 BY "app_a"."id" ASC LIMIT 1',
   'time': '0.001'}
 }}}

 Given  `Exists` has an `output_field` of `BooleanField` and that
 definition doesn't have `null=True` as an argument, it seems incongruent
 from both an expectations ("exists sounds boolean") and implementation
 ("it doesn't say it could be null") standpoint.


 ------------

 Whilst the problem exists in `main`, it has also changed behaviour
 (presumably or potentially unexpectedly) since `3.2`, where `postgres` and
 `sqlite` actually do different things, hence we tested both above. So
 `main` is now ''consistent'', but I'd personally argue it's consistently
 ''wrong'' (for a given value thereof, no judgement made!)

 In `3.2.16`, under sqlite, using `annotate(x=Exists(y.none()))` returns
 `False` but on `main` it now returns `None` (see above) -- the `3.2`
 behaviour is correct for ''my expectations''
 {{{
 In [4]:
 
A.objects.using("sqlite").annotate(should_be_bool=Exists(B.objects.none())).first().should_be_bool
 Out[4]: False
 In [5]: connections['sqlite'].queries
 Out[5]:
  {'sql': 'SELECT "app_a"."id", 0 AS "should_be_bool" FROM "app_a" ORDER BY
 "app_a"."id" ASC LIMIT 1',
   'time': '0.000'}
 }}}
 In `3.2.16` with postgres we get neither `None` nor `False` but the
 integer `0` instead:
 {{{
 In [4]:
 
A.objects.using("pg").annotate(should_be_bool=Exists(B.objects.none())).first().should_be_bool
 Out[4]: 0
 In [5]: connections['pg'].queries
 Out[5]:
 {'sql': 'SELECT "app_a"."id", 0 AS "should_be_bool" FROM "app_a" ORDER BY
 "app_a"."id" ASC LIMIT 1',
   'time': '0.001'}
 }}}

 -------------

 So we can observe that under `3.2` using the `0 AS ...` behaviour
 - sqlite appears to behave ''correctly'' (returning `False`)
 - postgres appears to behave incorrectly (failing to cast a raw integer to
 a boolean)

 And under `main` using the `NULL AS ...` behaviour
 - sqlite no longer behaves the same, returning `None` where I'd expect
 `False` (or even `True` given the way SQL EXISTS works...)
 - postgres behaves differently, `0` is now `None` but I'd still expect
 `True` or `False` as the output.

-- 
Ticket URL: <https://code.djangoproject.com/ticket/34254>
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 view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/01070185a5ef7b8a-9871bea2-9b92-4dc8-b29a-5bf9e1fbf215-000000%40eu-central-1.amazonses.com.

Reply via email to