#33992 closed Bug (fixed)
QuerySet.annotate() with subquery and aggregation crashes.
| Reported by: | Fernando Flores Villaça | Owned by: | Simon Charette |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 4.1 |
| Severity: | Release blocker | Keywords: | database, orm, aggregation |
| Cc: | Simon Charette | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
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
Change History (7)
comment:1 by , 3 years ago
| Cc: | added |
|---|---|
| Severity: | Normal → Release blocker |
| Triage Stage: | Unreviewed → Accepted |
comment:2 by , 3 years ago
| Summary: | Count subquery issue → QuerySet.annotate() with subquery and aggregation crashes. |
|---|
I was able to reproduce this issue with:
Post.objects.annotate(
is_following=Exists(
request_user.following.filter(id=OuterRef("user__id"))
),
).annotate(likes=Count("liked_by"))
comment:3 by , 3 years ago
| Has patch: | set |
|---|
comment:4 by , 3 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
| Triage Stage: | Accepted → Ready for checkin |
Note:
See TracTickets
for help on using tickets.
Thanks for the report. Regression in e5a92d400acb4ca6a8e1375d1ab8121f2c7220be.
I will try to reproduce this issue with a simpler models definition.