diff --git a/pyproject.toml b/pyproject.toml index b0f735f0d3..26f097f2fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,7 +169,7 @@ prod = [ "google-re2", # For querying recursive group membership - "django-cte", + "django-cte>=2.0.0.dev20250610173146", # https://github.com/dimagi/django-cte/pull/116 # SCIM integration "django-scim2", diff --git a/uv.lock b/uv.lock index df2c206cfb..8bd8a6470f 100644 --- a/uv.lock +++ b/uv.lock @@ -895,11 +895,14 @@ sdist = { url = "https://files.pythonhosted.org/packages/de/a2/7db0b55ff84260aa1 [[package]] name = "django-cte" -version = "1.3.3" +version = "2.0.0.dev20250610173146" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/56/951f6e176c83bf5e500ef7108c9e8de38785e249b62fa29be12b4c71c4eb/django-cte-1.3.3.tar.gz", hash = "sha256:0c1aeef067278a22886151c1d27f6f665a303952d058900e5ca82a24cde40697", size = 10931, upload-time = "2024-06-07T12:51:06.089Z" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/9c/7699e716095bc87151a1c8937c188d9734a341f414f4491e35375a669ba3/django_cte-2.0.0.dev20250610173146.tar.gz", hash = "sha256:4661c142def3c96b1a579ed23b813f44a3e81e1d68aaf03bb425583688cf538d", size = 11282, upload-time = "2025-06-10T17:32:10.299Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/02/a9d12e8c1cb767b2541bf7d759fbe713f84238f1c2bd22ed2ef3bed14606/django_cte-1.3.3-py2.py3-none-any.whl", hash = "sha256:85bbc3efb30c2f8c9ae3080ca6f0b9570e43d2cb4b6be10846c8ef9f046873fa", size = 11989, upload-time = "2024-06-07T12:51:04.827Z" }, + { url = "https://files.pythonhosted.org/packages/e3/15/1f508d7c05b250d0772141a1f020033723f71dd232e17d3a802b48f72bd5/django_cte-2.0.0.dev20250610173146-py3-none-any.whl", hash = "sha256:c75c97f6230b7af6ae364b3e970a7568e05706540fde6d155368bd92467cb2b1", size = 13166, upload-time = "2025-06-10T17:32:08.832Z" }, ] [[package]] @@ -1260,6 +1263,9 @@ dependencies = [ { name = "uritemplate" }, ] sdist = { url = "https://files.pythonhosted.org/packages/35/99/237cd2510aecca9fabb54007e58553274cc43cb3c18512ee1ea574d11b87/google_api_python_client-2.171.0.tar.gz", hash = "sha256:057a5c08d28463c6b9eb89746355de5f14b7ed27a65c11fdbf1d06c66bb66b23", size = 13028937, upload-time = "2025-06-03T18:57:38.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/db/c397e3eb3ea18f423855479d0a5852bdc9c3f644e3d4194931fa664a70b4/google_api_python_client-2.171.0-py3-none-any.whl", hash = "sha256:c9c9b76f561e9d9ac14e54a9e2c0842876201d5b96e69e48f967373f0784cbe9", size = 13547393, upload-time = "2025-06-10T02:14:38.225Z" }, +] [[package]] name = "google-auth" @@ -5492,7 +5498,7 @@ dev = [ { name = "django-auth-ldap" }, { name = "django-bitfield" }, { name = "django-bmemcached" }, - { name = "django-cte" }, + { name = "django-cte", specifier = ">=2.0.0.dev20250610173146" }, { name = "django-scim2" }, { name = "django-stubs" }, { name = "django-stubs-ext" }, @@ -5624,7 +5630,7 @@ prod = [ { name = "django-auth-ldap" }, { name = "django-bitfield" }, { name = "django-bmemcached" }, - { name = "django-cte" }, + { name = "django-cte", specifier = ">=2.0.0.dev20250610173146" }, { name = "django-scim2" }, { name = "django-stubs-ext" }, { name = "django-two-factor-auth", extras = ["call", "phonenumberslite", "sms"] }, diff --git a/version.py b/version.py index c8bd178633..5655a17940 100644 --- a/version.py +++ b/version.py @@ -49,4 +49,4 @@ API_FEATURE_LEVEL = 391 # historical commits sharing the same major version, in which case a # minor version bump suffices. -PROVISION_VERSION = (330, 0) # bumped 2025-06-05 to upgrade Python requirements +PROVISION_VERSION = (331, 0) # bumped 2025-06-10 to upgrade django-cte diff --git a/zerver/lib/message.py b/zerver/lib/message.py index cf197b384b..0b87c1af56 100644 --- a/zerver/lib/message.py +++ b/zerver/lib/message.py @@ -9,7 +9,7 @@ from django.db import connection from django.db.models import Exists, F, Max, OuterRef, QuerySet, Sum from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ -from django_cte import With +from django_cte import CTE, with_cte from psycopg2.sql import SQL from analytics.lib.counts import COUNT_STATS @@ -882,11 +882,10 @@ def get_raw_unread_data( # inside a CTE, such that the join to Recipients, below, can't be # implied to remove rows, and thus allows a Nested Loop join, # potentially memoized to reduce the number of Recipient lookups. - cte = With(user_msgs[:MAX_UNREAD_MESSAGES]) + cte = CTE(user_msgs[:MAX_UNREAD_MESSAGES]) user_msgs = ( - cte.join(Recipient, id=cte.col.recipient_id) - .with_cte(cte) + with_cte(cte, select=cte.join(Recipient, id=cte.col.recipient_id)) .annotate( message_id=cte.col.message_id, sender_id=cte.col.sender_id, diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index e6ab1e9fe4..0142360ae5 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -7,7 +7,7 @@ from django.db import connection, transaction from django.db.models import F, Q, QuerySet, Value from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ -from django_cte import With +from django_cte import CTE, with_cte from psycopg2.sql import SQL, Literal from zerver.lib.exceptions import ( @@ -780,23 +780,23 @@ def get_direct_memberships_of_users(user_group: UserGroup, members: list[UserPro def get_recursive_subgroups_union_for_groups(user_group_ids: list[int]) -> QuerySet[UserGroup]: - cte = With.recursive( + cte = CTE.recursive( lambda cte: UserGroup.objects.filter(id__in=user_group_ids) .values(group_id=F("id")) .union( cte.join(NamedUserGroup, direct_supergroups=cte.col.group_id).values(group_id=F("id")) ) ) - return cte.join(UserGroup, id=cte.col.group_id).with_cte(cte) + return with_cte(cte, select=cte.join(UserGroup, id=cte.col.group_id)) def get_recursive_supergroups_union_for_groups(user_group_ids: list[int]) -> QuerySet[UserGroup]: - cte = With.recursive( + cte = CTE.recursive( lambda cte: UserGroup.objects.filter(id__in=user_group_ids) .values(group_id=F("id")) .union(cte.join(UserGroup, direct_subgroups=cte.col.group_id).values(group_id=F("id"))) ) - return cte.join(UserGroup, id=cte.col.group_id).with_cte(cte) + return with_cte(cte, select=cte.join(UserGroup, id=cte.col.group_id)) def get_recursive_subgroups(user_group_id: int) -> QuerySet[UserGroup]: @@ -807,14 +807,14 @@ def get_recursive_strict_subgroups(user_group: UserGroup) -> QuerySet[NamedUserG # Same as get_recursive_subgroups but does not include the # user_group passed. direct_subgroup_ids = user_group.direct_subgroups.all().values("id") - cte = With.recursive( + cte = CTE.recursive( lambda cte: NamedUserGroup.objects.filter(id__in=direct_subgroup_ids) .values(group_id=F("id")) .union( cte.join(NamedUserGroup, direct_supergroups=cte.col.group_id).values(group_id=F("id")) ) ) - return cte.join(NamedUserGroup, id=cte.col.group_id).with_cte(cte) + return with_cte(cte, select=cte.join(NamedUserGroup, id=cte.col.group_id)) def get_recursive_group_members(user_group_id: int) -> QuerySet[UserProfile]: @@ -831,12 +831,12 @@ def get_recursive_group_members_union_for_groups( def get_recursive_membership_groups(user_profile: UserProfile) -> QuerySet[UserGroup]: - cte = With.recursive( + cte = CTE.recursive( lambda cte: user_profile.direct_groups.values(group_id=F("id")).union( cte.join(UserGroup, direct_subgroups=cte.col.group_id).values(group_id=F("id")) ) ) - return cte.join(UserGroup, id=cte.col.group_id).with_cte(cte) + return with_cte(cte, select=cte.join(UserGroup, id=cte.col.group_id)) def user_has_permission_for_group_setting( @@ -899,14 +899,14 @@ def get_subgroup_ids(user_group: UserGroup, *, direct_subgroup_only: bool = Fals def get_recursive_subgroups_for_groups( user_group_ids: Iterable[int], realm: Realm ) -> QuerySet[NamedUserGroup]: - cte = With.recursive( + cte = CTE.recursive( lambda cte: NamedUserGroup.objects.filter(id__in=user_group_ids, realm=realm) .values(group_id=F("id")) .union( cte.join(NamedUserGroup, direct_supergroups=cte.col.group_id).values(group_id=F("id")) ) ) - recursive_subgroups = cte.join(NamedUserGroup, id=cte.col.group_id).with_cte(cte) + recursive_subgroups = with_cte(cte, select=cte.join(NamedUserGroup, id=cte.col.group_id)) return recursive_subgroups @@ -916,7 +916,7 @@ def get_root_id_annotated_recursive_subgroups_for_groups( # Same as get_recursive_subgroups_for_groups but keeps track of # each group root_id and annotates it with that group. - cte = With.recursive( + cte = CTE.recursive( lambda cte: UserGroup.objects.filter(id__in=user_group_ids, realm=realm_id) .values(group_id=F("id"), root_id=F("id")) .union( @@ -925,8 +925,8 @@ def get_root_id_annotated_recursive_subgroups_for_groups( ) ) ) - recursive_subgroups = ( - cte.join(UserGroup, id=cte.col.group_id).with_cte(cte).annotate(root_id=cte.col.root_id) + recursive_subgroups = with_cte(cte, select=cte.join(UserGroup, id=cte.col.group_id)).annotate( + root_id=cte.col.root_id ) return recursive_subgroups diff --git a/zerver/models/groups.py b/zerver/models/groups.py index 9279a61784..635c56ec22 100644 --- a/zerver/models/groups.py +++ b/zerver/models/groups.py @@ -2,7 +2,6 @@ from django.db import models from django.db.models import CASCADE from django.utils.timezone import now as timezone_now from django.utils.translation import gettext_lazy -from django_cte import CTEManager from zerver.lib.cache import cache_with_key, get_realm_system_groups_cache_key from zerver.lib.types import GroupPermissionSetting @@ -31,8 +30,7 @@ class SystemGroups: } -class UserGroup(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 - objects: CTEManager = CTEManager() +class UserGroup(models.Model): direct_members = models.ManyToManyField( UserProfile, through="zerver.UserGroupMembership", related_name="direct_groups" ) @@ -46,7 +44,7 @@ class UserGroup(models.Model): # type: ignore[django-manager-missing] # django- realm = models.ForeignKey("zerver.Realm", on_delete=CASCADE) -class NamedUserGroup(UserGroup): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 +class NamedUserGroup(UserGroup): MAX_NAME_LENGTH = 100 INVALID_NAME_PREFIXES = ["@", "role:", "user:", "stream:", "channel:"] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index 9a7fb727d5..8e637b3a1b 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -155,7 +155,7 @@ class MessageEditHistoryVisibilityPolicyEnum(Enum): none = 3 -class Realm(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 +class Realm(models.Model): MAX_REALM_NAME_LENGTH = 40 MAX_REALM_DESCRIPTION_LENGTH = 1000 MAX_REALM_SUBDOMAIN_LENGTH = 40 diff --git a/zerver/models/recipients.py b/zerver/models/recipients.py index 0dbcaafed9..632151ba58 100644 --- a/zerver/models/recipients.py +++ b/zerver/models/recipients.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING from django.db import models, transaction from django.db.models import QuerySet -from django_cte import CTEManager from typing_extensions import override from zerver.lib.display_recipient import get_display_recipient @@ -53,8 +52,6 @@ class Recipient(models.Model): # The type group direct messages. DIRECT_MESSAGE_GROUP = 3 - objects = CTEManager() # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 - class Meta: unique_together = ("type", "type_id")