Repository: wagtail/wagtail

Issue:
When using alias(), Wagtail tries to select the value in SpecificIterable as if annotate() was used
### Issue Summary

When using the [alias()](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#alias) method on a Page QuerySet with specific(), Wagtail tries to select the aliased field as it was an annotation.

I've submitted a pull request as well - #11286.

### Steps to Reproduce

E.g. this code breaks:

```python
from django.db.models.functions import Lower
from wagtail.models import Page
Page.objects.alias(lower_title=Lower("title")).specific().order_by("title")
```

The issue seems to be that Wagtail does [not filter QuerySet's annotation keys](https://github.com/wagtail/wagtail/blob/49a22669207a88bbba70619dc4eac04df65404cb/wagtail/query.py#L518-L519) with the [annotation_select_mask](https://github.com/django/django/blob/101a85a5a06585ba16ecb25860146d034a8a55ec/django/db/models/sql/query.py#L2502-L2507).

```python
>>> Page.objects.alias(lower_title=Lower("title")).specific().order_by("title")
Traceback (most recent call last):
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 2161, in add_fields
    join_info = self.setup_joins(
                ^^^^^^^^^^^^^^^^^
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1823, in setup_joins
    path, final_field, targets, rest = self.names_to_path(
                                       ^^^^^^^^^^^^^^^^^^^
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 1724, in names_to_path
    raise FieldError(
django.core.exceptions.FieldError: Cannot resolve keyword 'lower_title' into field. Choices are: _revisions, _specific_workflow_states, _workflow_states, alias_of, alias_of_id, aliases, content_type, content_type_id, depth, differentpage, draft_title, examplepage, expire_at, expired, first_published_at, formsubmission, go_live_at, group_permissions, has_unpublished_changes, id, index_entries, last_published_at, latest_revision, latest_revision_created_at, latest_revision_id, live, live_revision, live_revision_id, locale, locale_id, locked, locked_at, locked_by, locked_by_id, numchild, owner, owner_id, path, redirect, search_description, searchpromotion, seo_title, show_in_menus, sites_rooted_here, slug, subscribers, title, translation_key, url_path, view_restrictions, wagtail_admin_comments, workflowpage

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/query.py", line 374, in __repr__
    data = list(self[: REPR_OUTPUT_SIZE + 1])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/query.py", line 398, in __iter__
    self._fetch_all()
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1881, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/wagtail/query.py", line 519, in __iter__
    values_qs = qs.values("pk", "content_type", *annotation_aliases)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1313, in values
    clone = self._values(*fields, **expressions)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/query.py", line 1308, in _values
    clone.query.set_values(fields)
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 2460, in set_values
    self.add_fields(field_names, True)
  File "/Users/tomaszknapik/Developer/wagtail-vector-index/venv/lib/python3.11/site-packages/django/db/models/sql/query.py", line 2181, in add_fields
    raise FieldError(
django.core.exceptions.FieldError: Cannot select the 'lower_title' alias. Use annotate() to promote it.
>>> Page.objects.annotate(lower_title=Lower("title")).specific().order_by("title")
<PageQuerySet [<Page: Root>, <ExamplePage: test>, <Page: Welcome to your new Wagtail site!>]>
```

### Technical details

-   Python version: 3.11.6
-   Django version: 4.2.7
-   Wagtail version: 5.2.1



Discussion:

Resolving patch:
diff --git a/wagtail/query.py b/wagtail/query.py
--- a/wagtail/query.py
+++ b/wagtail/query.py
@@ -515,7 +515,7 @@ def __iter__(self):
         in the same order, with any annotations intact.
         """
         qs = self.queryset
-        annotation_aliases = qs.query.annotations.keys()
+        annotation_aliases = qs.query.annotation_select
         values_qs = qs.values("pk", "content_type", *annotation_aliases)
 
         # Gather items in batches to reduce peak memory usage


Test patch:
diff --git a/wagtail/tests/test_page_queryset.py b/wagtail/tests/test_page_queryset.py
--- a/wagtail/tests/test_page_queryset.py
+++ b/wagtail/tests/test_page_queryset.py
@@ -919,6 +919,25 @@ def test_specific_query_with_annotation(self):
         self.assertEqual(results.first().subscribers_count, 1)
         self.assertEqual(results.last().subscribers_count, 1)
 
+    def test_specific_query_with_alias(self):
+        """
+        Ensure alias() works with specific() queries.
+        See https://github.com/wagtail/wagtail/issues/11285 for more details
+        """
+
+        pages = Page.objects.live()
+        user = self.create_test_user()
+        pages.first().subscribers.create(user=user, comment_notifications=False)
+        pages.last().subscribers.create(user=user, comment_notifications=False)
+
+        # This would previously fail as described in #11285.
+        iter(
+            Page.objects.live()
+            .specific()
+            .alias(subscribers_count=Count("subscribers"))
+            .order_by("subscribers_count")
+        )
+
     def test_specific_gracefully_handles_missing_models(self):
         # 3567 - PageQuerySet.specific should gracefully handle pages whose class definition
         # is missing, by keeping them as basic Page instances.