Skip to content

fix(api): annotate WorkspaceModulesEndpoint to avoid per-row lookups (N+1)#9378

Open
duanearnett wants to merge 1 commit into
makeplane:previewfrom
duanearnett:fix/workspace-modules-annotations
Open

fix(api): annotate WorkspaceModulesEndpoint to avoid per-row lookups (N+1)#9378
duanearnett wants to merge 1 commit into
makeplane:previewfrom
duanearnett:fix/workspace-modules-annotations

Conversation

@duanearnett

@duanearnett duanearnett commented Jul 8, 2026

Copy link
Copy Markdown

Summary

WorkspaceModulesEndpoint (GET /api/workspaces/<slug>/modules/) serializes with ModuleSerializer, but builds a queryset that omits several computed fields the serializer exposes: member_ids, is_favorite, total_estimate_points, and completed_estimate_points.

The project-scoped ModuleViewSet.get_queryset() already annotates all of these (ArrayAgg for member_ids, Exists for is_favorite, Subquery for estimate points). Because the workspace endpoint does not, those fields end up resolved per row during serialization — most notably a separate members lookup for every module in the workspace. On workspaces with many modules this makes the endpoint scale as O(number of modules) database queries instead of a constant number, and it's a common source of slow initial page loads.

Fix

Annotate the same computed fields on the workspace queryset, mirroring the existing project-scoped ModuleViewSet implementation:

  • is_favoriteExists(...)
  • member_idsCoalesce(ArrayAgg("members__id", distinct=True, filter=...), [])
  • total_estimate_points / completed_estimate_pointsSubquery(...)

The existing issue-count annotations are left unchanged to preserve current behavior; this PR only adds the missing annotations so no serializer field falls back to a per-row lookup. Net effect: the module list is served in a constant number of queries regardless of module count.

Notes

  • No API contract change — the serialized fields are identical, just sourced from annotations instead of per-row access.
  • Pattern is a direct port of plane/app/views/module/base.py::ModuleViewSet.get_queryset.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Workspace modules now return more reliable favorite status, member lists, and progress/estimate totals in the UI.
    • Completed and total estimate points are now calculated more consistently for each module.
  • Performance
    • Module details are now prepared more efficiently, helping reduce load time for workspace module views.

The workspace-scoped modules list feeds ModuleSerializer a queryset that
omits several computed fields the serializer exposes (member_ids,
is_favorite, total/completed_estimate_points). The project-scoped
ModuleViewSet.get_queryset already annotates these via ArrayAgg / Exists /
Subquery; the workspace endpoint did not, so those fields are resolved per
row during serialization -> one members lookup per module (N+1) on the
workspace modules list.

Add the same annotations here so the list is served in a constant number
of queries regardless of module count. Issue-count annotations are left
unchanged to preserve existing behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


duane seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The WorkspaceModulesEndpoint's get method is updated to precompute additional fields at the queryset level via ORM subqueries: is_favorite existence check, member_ids array aggregation, and total/completed estimate point sums, reducing per-row serialization work.

Changes

Workspace Modules Queryset Precomputation

Layer / File(s) Summary
Subquery definitions and imports
apps/api/plane/app/views/workspace/module.py
New imports (ArrayAgg, ArrayField, Exists, OuterRef, Subquery, Sum, Cast, Coalesce) support new subqueries for favorite existence and total/completed estimate-point aggregates from filtered Issue queries.
Queryset annotation wiring
apps/api/plane/app/views/workspace/module.py
The modules queryset gains is_favorite, member_ids (distinct array with null-to-empty coalescing), and estimate-point annotations using the defined subqueries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding queryset annotations to avoid per-row lookups in WorkspaceModulesEndpoint.
Description check ✅ Passed The description is detailed and covers the summary, fix, and notes, but it omits several template sections like type of change, tests, and references.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/plane/app/views/workspace/module.py (1)

85-108: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Cartesian-product cost from mixing ArrayAgg (member_ids) with the Count issue annotations.

member_ids adds a members M2M join to the same queryset that already fans out over issue_module for the six issue-count annotations, producing members × issue_modules rows per module. The distinct=True guards keep the counts and array correct, but the intermediate result set (and grouping cost) grows multiplicatively. This mirrors ModuleViewSet, so it's likely acceptable, but worth confirming it doesn't regress list latency on large workspaces; isolating member_ids into its own Subquery(ArrayAgg(...)) avoids the extra join entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/views/workspace/module.py` around lines 85 - 108, The
queryset in module aggregation is introducing a multiplicative join by combining
member_ids ArrayAgg with the existing issue count annotations, which can inflate
the intermediate row set. Update the annotation logic around member_ids in the
workspace module queryset to avoid joining members on the same query as the
issue_module-based counts, preferably by moving member_ids into its own
Subquery(ArrayAgg(...)) or equivalent isolated annotation. Keep the existing
count annotations in place, and use the Module queryset structure as the
reference point when adjusting the annotate chain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/app/views/workspace/module.py`:
- Around line 86-98: The member_ids aggregation is currently joining through
members__id and modulemember__deleted_at separately, which can allow
soft-deleted memberships to leak into the result. Update the annotate/ArrayAgg
in the module query to aggregate directly from the ModuleMember path, keeping
the deleted_at__isnull=True filter on that same join and preserving the distinct
UUID list behavior.

---

Nitpick comments:
In `@apps/api/plane/app/views/workspace/module.py`:
- Around line 85-108: The queryset in module aggregation is introducing a
multiplicative join by combining member_ids ArrayAgg with the existing issue
count annotations, which can inflate the intermediate row set. Update the
annotation logic around member_ids in the workspace module queryset to avoid
joining members on the same query as the issue_module-based counts, preferably
by moving member_ids into its own Subquery(ArrayAgg(...)) or equivalent isolated
annotation. Keep the existing count annotations in place, and use the Module
queryset structure as the reference point when adjusting the annotate chain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04bf223d-3b1b-4f4e-b7e0-347f5f429a90

📥 Commits

Reviewing files that changed from the base of the PR and between 4fc79a2 and c4d2fcb.

📒 Files selected for processing (1)
  • apps/api/plane/app/views/workspace/module.py

Comment on lines +86 to +98
.annotate(
member_ids=Coalesce(
ArrayAgg(
"members__id",
distinct=True,
filter=Q(
members__id__isnull=False,
modulemember__deleted_at__isnull=True,
),
),
Value([], output_field=ArrayField(UUIDField())),
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect Module.members M2M + ModuleMember through model, and compare member_ids annotation with ModuleViewSet.
fd -t f 'module.py' apps/api/plane/db/models | while read -r f; do
  echo "=== $f ==="
  rg -nP -C3 '(class Module\b|class ModuleMember\b|members\s*=\s*models\.ManyToManyField|through=|related_name=)' "$f"
done

echo "===== ModuleViewSet member_ids / through-name usage ====="
rg -nP -C4 'member_ids\s*=' apps/api/plane/app/views
rg -nP -C1 'module_members|modulemember' apps/api/plane/app/views/module apps/api/plane/db/models

Repository: makeplane/plane

Length of output: 6397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== ModuleMember model and inheritance ==="
sed -n '1,220p' apps/api/plane/db/models/module.py | cat -n

echo "=== Workspace module view ==="
sed -n '1,180p' apps/api/plane/app/views/workspace/module.py | cat -n

echo "=== Base module view ==="
sed -n '250,310p' apps/api/plane/app/views/module/base.py | cat -n

echo "=== Archive module view ==="
sed -n '235,265p' apps/api/plane/app/views/module/archive.py | cat -n

echo "=== Search for ModuleMember reverse-name usage ==="
rg -n "modulemember__|module_members__|modulemember_set|ModuleMember" apps/api/plane -g '!**/__pycache__/**'

Repository: makeplane/plane

Length of output: 22962


Aggregate from the through model on one join. members__id plus modulemember__deleted_at can split across two ModuleMember joins, so deleted memberships may still appear in member_ids. Switch the aggregate to the ModuleMember path and keep the soft-delete filter there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/views/workspace/module.py` around lines 86 - 98, The
member_ids aggregation is currently joining through members__id and
modulemember__deleted_at separately, which can allow soft-deleted memberships to
leak into the result. Update the annotate/ArrayAgg in the module query to
aggregate directly from the ModuleMember path, keeping the
deleted_at__isnull=True filter on that same join and preserving the distinct
UUID list behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants