#33992: Count subquery issue
-------------------------------------+-------------------------------------
               Reporter:  Fernando   |          Owner:  nobody
  Flores Villaça                     |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  4.1
  layer (models, ORM)                |       Keywords:  database, orm,
               Severity:  Normal     |  aggregation
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 I updated one app from 4.0 to 4.1.1 and found a issue with one annotation
 using `Count`. I tested with SQLite and PostgreSQL, and both raised
 exception. The same app works with 4.0.7.

 Exception with SQLite:
 {{{
 sub-select returns 13 columns - expected 1
 Traceback (most recent call last):
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 89, in _execute
     return self.cursor.execute(sql, params)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/sqlite3/base.py", line 357, in execute
     return Database.Cursor.execute(self, query, params)
 sqlite3.OperationalError: sub-select returns 13 columns - expected 1

 The above exception was the direct cause of the following exception:

 Traceback (most recent call last):
   File ".../.venv/lib/python3.10/site-packages/django/db/models/query.py",
 line 1225, in exists
     return self.query.has_results(using=self.db)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/models/sql/query.py", line 592, in has_results
     return compiler.has_results()
   File ".../.venv/lib/python3.10/site-
 packages/django/db/models/sql/compiler.py", line 1363, in has_results
     return bool(self.execute_sql(SINGLE))
   File ".../.venv/lib/python3.10/site-
 packages/django/db/models/sql/compiler.py", line 1395, in execute_sql
     cursor.execute(sql, params)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 103, in execute
     return super().execute(sql, params)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 67, in execute
     return self._execute_with_wrappers(
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
     return executor(sql, params, many, context)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 84, in _execute
     with self.db.wrap_database_errors:
   File ".../.venv/lib/python3.10/site-packages/django/db/utils.py", line
 91, in __exit__
     raise dj_exc_value.with_traceback(traceback) from exc_value
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 89, in _execute
     return self.cursor.execute(sql, params)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/sqlite3/base.py", line 357, in execute
     return Database.Cursor.execute(self, query, params)
 django.db.utils.OperationalError: sub-select returns 13 columns - expected
 1
 }}}

 Exception with Postgres:
 {{{
 subquery must return only one column
 LINE 1: ...iked_by"."post_id") GROUP BY "network_post"."id", (SELECT U0...
                                                              ^
 Traceback (most recent call last):
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 89, in _execute
     return self.cursor.execute(sql, params)
 psycopg2.errors.SyntaxError: subquery must return only one column
 LINE 1: ...iked_by"."post_id") GROUP BY "network_post"."id", (SELECT U0...
                                                              ^

 The above exception was the direct cause of the following exception:

 Traceback (most recent call last):
   File ".../.venv/lib/python3.10/site-packages/django/db/models/query.py",
 line 1225, in exists
     return self.query.has_results(using=self.db)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/models/sql/query.py", line 592, in has_results
     return compiler.has_results()
   File ".../.venv/lib/python3.10/site-
 packages/django/db/models/sql/compiler.py", line 1363, in has_results
     return bool(self.execute_sql(SINGLE))
   File ".../.venv/lib/python3.10/site-
 packages/django/db/models/sql/compiler.py", line 1395, in execute_sql
     cursor.execute(sql, params)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 103, in execute
     return super().execute(sql, params)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 67, in execute
     return self._execute_with_wrappers(
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
     return executor(sql, params, many, context)
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 84, in _execute
     with self.db.wrap_database_errors:
   File ".../.venv/lib/python3.10/site-packages/django/db/utils.py", line
 91, in __exit__
     raise dj_exc_value.with_traceback(traceback) from exc_value
   File ".../.venv/lib/python3.10/site-
 packages/django/db/backends/utils.py", line 89, in _execute
     return self.cursor.execute(sql, params)
 django.db.utils.ProgrammingError: subquery must return only one column
 LINE 1: ...iked_by"."post_id") GROUP BY "network_post"."id", (SELECT U0...
                                                              ^
 }}}

 The exception is raised by `annotate(likes=Count("liked_by"))` in method
 `fetch_all_posts`.
 {{{
 class PostManager(models.Manager):
     def request_data(self, request_user):
         liked_by_user = Value(False)
         is_following = Value(False)
         is_owner = Case(When(user__id=request_user.id, then=True),
 default=False)

         if request_user.is_authenticated:
             # Check if the user has liked the post in each row of the
 query
             liked_by_user =
 Exists(request_user.liked_posts.filter(id=OuterRef("id")))
             is_following = Exists(
                 request_user.following.filter(id=OuterRef("user__id"))
             )

         return is_owner, liked_by_user, is_following

     def fetch_all_posts(self, request_user) -> QuerySet[Post]:
         is_owner, liked_by_user, is_following =
 self.request_data(request_user)

         return (
             self.select_related()
             .prefetch_related(
                 Prefetch(
                     "comments",
 queryset=Comment.objects.select_related().filter(reply=False),
                 ),  # filter related "comments" inside the post QuerySet
             )
             .order_by("-publication_date")
             .annotate(is_following=is_following)
             .annotate(is_owner=is_owner)
             .annotate(likes=Count("liked_by"))  # Doesn't work on 4.1
             .annotate(liked_by_user=liked_by_user)
         )

     def fetch_following_posts(self, request_user: User) -> QuerySet[Post]:
         return self.fetch_all_posts(request_user).filter(
             user__in=request_user.following.all()
         )
 }}}

 Models
 {{{
 class User(AbstractUser):
     id: int
     posts: RelatedManager[Post]
     liked_posts: RelatedManager[Post]
     comments: RelatedManager[Comment]

     about = models.CharField(blank=True, max_length=255)
     photo = models.ImageField(
         blank=True,
         null=True,
         upload_to=upload_path,
         validators=[file_validator],
     )

     following = models.ManyToManyField(
         "self", related_name="followers", symmetrical=False
     )

     objects: CustomUserManager = CustomUserManager()

     # Related fields
     # posts = ManyToOne("Post", related_name="user")
     # liked_posts = ManyToMany("Post", related_name="liked_by")
     # comments = ManyToOne("Comment", related_name="user")

     def save(self, *args, **kwargs):
         """
         full_clean is not called automatically on save by Django
         """
         self.full_clean()
         super().save(*args, **kwargs)

     def __str__(self):
         return f"{self.username}"  # type: ignore


 class Post(models.Model):
     id: int
     comments: RelatedManager[Comment]

     user_id: int
     user = models.ForeignKey(
         settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
 related_name="posts"
     )
     text = models.CharField(max_length=200)
     publication_date = models.DateTimeField(auto_now_add=True)
     edited = models.BooleanField(default=False)
     last_modified = models.DateTimeField(auto_now_add=True)
     liked_by = models.ManyToManyField(
         settings.AUTH_USER_MODEL, related_name="liked_posts", blank=True
     )

     # Related Fields
     # comments = ManyToOne("Comment", related_name="post")

     objects: PostManager = PostManager()

     class Meta:
         ordering = ["-publication_date"]

     def __str__(self):
         return f"{self.text}"


 class Comment(models.Model):
     id: int
     replies: RelatedManager[Comment]

     post_id: int
     post = models.ForeignKey(Post, on_delete=models.CASCADE,
 related_name="comments")
     user_id: int
     user = models.ForeignKey(
         settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
 related_name="comments"
     )
     text = models.CharField(max_length=200)
     publication_date = models.DateTimeField(auto_now_add=True)
     reply = models.BooleanField(default=False)
     parent_comment_id: int
     parent_comment = models.ForeignKey(
         "self", on_delete=models.CASCADE, null=True, blank=True,
 related_name="replies"
     )

     class Meta:
         ordering = ["-publication_date"]

     def save(self, *args, **kwargs):
         self.full_clean()
         if self.parent_comment is not None:
             if self.parent_comment.post.id != self.post.id:
                 raise ValidationError("Parent comment must be from the
 same post.")
             self.reply = True
         super().save(*args, **kwargs)

     def __str__(self):
         return f"{self.text} - reply: {self.reply}"  # type: ignore
 }}}

-- 
Ticket URL: <https://code.djangoproject.com/ticket/33992>
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/0107018318d659a2-f39167f4-1c1a-4099-8993-e37a4d581b59-000000%40eu-central-1.amazonses.com.

Reply via email to