⚡ Resolve N+1 Query in Data Export for Conversation Photos - #12491
⚡ Resolve N+1 Query in Data Export for Conversation Photos#12491undivisible wants to merge 5 commits into
Conversation
Failure-Class: none Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Failure-Class: none Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Line-Count-Exception: backend/database/conversations.py | 2069 -> 2085 | Added efficiently optimized iteration function iter_all_conversation_photos for large accounts. Failure-Class: none Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Git-on-my-level
left a comment
There was a problem hiding this comment.
Thanks @undivisible - the N+1 elimination direction is right, and the tenant scoping is done correctly: I verified the __name__ range bounds in iter_all_conversation_photos exclude sibling-uid prefixes (e.g. users/{uid}X/...) and shorter uids, and the trailing-path guard drops deeper-nested docs that fall inside the range. Two verified blockers before merge, though.
1. Hygiene is red: product-file-line-count-ratchet. backend/database/conversations.py grows 2069 -> 2085 lines and the file is frozen by #9838. Either move the helper, or paste this exact line into the PR body:
Line-Count-Exception: backend/database/conversations.py | 2069 -> 2085 | <reason>
2. Latent: the new query shape is unregistered and will keep Hygiene red once (1) is fixed. I ran the repo's own backend/scripts/firestore_query_coverage.py --check-ratchet at this head:
ERROR: new unregistered serving compound query shape(s): 94464b27cc4d608e - backend/database/conversations.py:388 photos [__name__ >=, __name__ <=].
It is masked in CI right now only because the ratchet in (1) fails first. A collection-group query with range filters also needs a collection-group-scoped index on photos - firestore.indexes.json has no photos entry today, so the first production export would come back FAILED_PRECONDITION (query requires an index) instead of data. Please register the shape in backend/database/firestore_index_registry.py (QUERY_SPECS) and add the photos __name__ COLLECTION_GROUP index to firestore.indexes.json, or record a waiver if this is intentional transitional debt.
Per-file notes:
backend/database/conversations.py-iter_all_conversation_photos(lines 384-399): range construction and theparts[-2] == 'photos'guard are correct; the registration/index gap above is the only issue.backend/services/users/data_export.py- hoisting the photo loop out of the conversations loop preserves the spooling,photo_count, andrequire_bytes=Falseportability semantics. One subtle behavior change worth a line in the PR body: the collection-group scan now also picks up photo docs whose parent conversation doc no longer exists (orphaned subcollections), where the old per-conversation loop only saw photos of exported conversations. For a portability export that is arguably more complete - just make it explicit.backend/tests/services/users/test_data_export.py- the mock updates to the(conversation_id, photo)tuple shape are consistent across the fixture and both affected tests. Nothing here (or in e2e - the fake'scollection_groupsupport is explicitly optional) executes the real query construction, which is why the index requirement in (2) was caught by no suite.backend/tests/unit/test_daily_memory_sweep.py- the one-line stub ofiter_all_conversation_photoscorrectly keeps that suite hermetic.
Once the exception line and the registration/index (or waiver) land, the remaining judgment call for a maintainer is deploying a new collection-group index on photos - that production-infrastructure step needs maintainer sign-off.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
kodjima33
left a comment
There was a problem hiding this comment.
Approve only: N+1 perf-rewrite (not a bug fix) and PR Metadata Preflight check is failing — hard floor blocks merge either way.
Record the export __name__ range shape in QUERY_SPECS so Hygiene's firestore query coverage ratchet sees it. No firestore.indexes.json change — index deploy stays a maintainer step.
|
Status update on head Resolved - line-count ratchet. The Newly red -
Paste-ready PR-body fix: A one-line justification there would help reviewers: the export path is locked to INV-MEM-3 because it reads memory-adjacent user data, and this change only swaps the photo subcollection read strategy - no hydration or memory-authority decisions are touched. Still open - the new query shape is unregistered and unindexed (re-verified at this head).
The approval covers the perf-rewrite direction. What's left is mechanical: the INV-MEM-3 declaration, the query-shape registration, and the index entry - once those land, the new by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with |
|
QUERY_SPECS registered; indexes.json regenerated from registry; index is not deployed. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_25dc0a9f-02dd-4879-a0bb-6e2fd9db38d4) |
|
Follow-up on head Resolved - query-shape registration and index. One stale sentence in the PR body. It still says the index is "registered in QUERY_SPECS but not added to firestore.indexes.json in this PR (no index deploy)" - that was accurate before Remaining before merge - deploy the index. The repo file is only the manifest; the production Per-file notes at this head:
The changes-requested review from the earlier head has been dismissed as resolved; @kodjima33's approval already covers the direction. by AI on behalf of David - remaining maintainer step for this PR: ship the |
Dismissed as resolved on head 350e51b: the query shape is now registered in QUERY_SPECS (CONVERSATION_PHOTOS_NAME_RANGE_QUERY) and the photos name COLLECTION_GROUP index is in firestore.indexes.json, both requested blockers from this review; the line-count exception line is in the PR body and Hygiene is green.
💡 What: Replaced the iterative
get_conversation_photoscall (which executes one DB query per conversation) with a singleiter_all_conversation_photosfunction that utilizes a scopedcollection_groupquery on thephotoscollection to efficiently retrieve all photos for a user's conversations in a single batched stream.🎯 Why: The previous implementation executed a Firestore subcollection query for each conversation the user had. If a user had 1,000 conversations, the export process would execute 1,000 separate DB queries. This N+1 anti-pattern resulted in excessive latency and potential timeouts for large accounts.
📊 Measured Improvement:
Product invariants affected
Line-Count-Exception: backend/database/conversations.py | 2069 -> 2085 | collection-group photo export helper to eliminate N+1
The collection-group scan also includes photo docs whose parent conversation no longer exists (orphaned subcollections); the old per-conversation loop only exported photos of exported conversations. That is intentional for a more complete portability export.
Firestore index for
photos__name__COLLECTION_GROUP is registered in QUERY_SPECS but not added to firestore.indexes.json in this PR (no index deploy). Production export of this shape still needs a maintainer-deployed collection-group index.Failure-Class: none
PR created automatically by Jules for task 13867765088328126868 started by @undivisible
Note
Medium Risk
Touches user data export and a new collection-group read path; export now uses raw
to_dict()from the batch iterator rather than the@prepare_for_readpath onget_conversation_photos, which may affect enhanced-encryption photo fields until aligned.Overview
Eliminates N+1 Firestore reads when building the user portability export’s
conversation_photo_manifest. Instead of callingget_conversation_photosonce per exported conversation, export now streams all photos through a newiter_all_conversation_photoshelper after the conversations array is written.The helper runs a user-scoped
collection_group('photos')query bounded by document-ID range (__name__between path prefixes underusers/{uid}/conversations/...) and yields(conversation_id, photo_dict)pairs parsed from each doc path.Index plumbing: registers
CONVERSATION_PHOTOS_NAME_RANGE_QUERYin the Firestore index registry and adds the matchingphotosCOLLECTION_GROUP composite on__name__infirestore.indexes.json.Export semantics: photo manifest collection is decoupled from the conversation loop, so manifests can include photos on orphaned conversation subcollections (not only conversations returned by
iter_all_conversations)—intentionally broader portability coverage. Tests and default test fixtures were updated to mockiter_all_conversation_photosinstead ofget_conversation_photos.Reviewed by Cursor Bugbot for commit 350e51b. Configure here.