From 5908afb098c91fa1cac2faeead517802b84aecc4 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Thu, 16 Oct 2025 17:06:47 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(backend)=20improve=20trashbi?= =?UTF-8?q?n=20endpoint=20performance=20(#1495)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trashbin endpoint is slow. To filter documents the user has owner access, we use a subquery to compute the roles and then filter on this subquery. This is very slow. To improve it, we use the same way to filter children used in the tree endpoint. First we look for all highest ancestors the user has access on with the owner role. Then we create one queryset filtering on all the docs starting by the given path and are deleted. --- CHANGELOG.md | 4 ++++ src/backend/core/api/viewsets.py | 19 ++++++++++++++++++- .../documents/test_api_documents_trashbin.py | 8 ++++---- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dd8ba06..5d410d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to - ♿ add missing aria-label to add sub-doc button for accessib… #1480 - ♿ add missing aria-label to more options button on sub-docs #1481 +### Fixed + +- ⚡️(backend) improve trashbin endpoint performance + ## [3.8.0] - 2025-10-14 ### Added diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 2cd45e62..009d9683 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -623,12 +623,29 @@ class DocumentViewSet( The selected documents are those deleted within the cutoff period defined in the settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted. """ + + if not request.user.is_authenticated: + return self.get_response_for_queryset(self.queryset.none()) + + access_documents_paths = ( + models.DocumentAccess.objects.select_related("document") + .filter( + db.Q(user=self.request.user) | db.Q(team__in=self.request.user.teams), + role=models.RoleChoices.OWNER, + ) + .values_list("document__path", flat=True) + ) + + children_clause = db.Q() + for path in access_documents_paths: + children_clause |= db.Q(path__startswith=path) + queryset = self.queryset.filter( + children_clause, deleted_at__isnull=False, deleted_at__gte=models.get_trashbin_cutoff(), ) queryset = queryset.annotate_user_roles(self.request.user) - queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER]) return self.get_response_for_queryset(queryset) diff --git a/src/backend/core/tests/documents/test_api_documents_trashbin.py b/src/backend/core/tests/documents/test_api_documents_trashbin.py index fbcc2317..ffdffd43 100644 --- a/src/backend/core/tests/documents/test_api_documents_trashbin.py +++ b/src/backend/core/tests/documents/test_api_documents_trashbin.py @@ -166,10 +166,10 @@ def test_api_documents_trashbin_authenticated_direct(django_assert_num_queries): expected_ids = {str(document1.id), str(document2.id), str(document3.id)} - with django_assert_num_queries(10): + with django_assert_num_queries(11): response = client.get("/api/v1.0/documents/trashbin/") - with django_assert_num_queries(4): + with django_assert_num_queries(5): response = client.get("/api/v1.0/documents/trashbin/") assert response.status_code == 200 @@ -208,10 +208,10 @@ def test_api_documents_trashbin_authenticated_via_team( expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)} - with django_assert_num_queries(7): + with django_assert_num_queries(8): response = client.get("/api/v1.0/documents/trashbin/") - with django_assert_num_queries(3): + with django_assert_num_queries(4): response = client.get("/api/v1.0/documents/trashbin/") assert response.status_code == 200