fix(api): resolve the instrument subject filter without a relation join#1423
Open
gdevenyi wants to merge 1 commit into
Open
fix(api): resolve the instrument subject filter without a relation join#1423gdevenyi wants to merge 1 commit into
gdevenyi wants to merge 1 commit into
Conversation
`GET /v1/instruments/info?subjectId=` returns HTTP 500, and the datahub
subject view stops rendering entirely, once a single instrument
accumulates roughly 182,000 records.
`InstrumentsService.find` expressed "instruments this subject has records
for" as a prisma relation filter:
records: { some: { subjectId } }
On mongodb prisma compiles that into a $lookup which materialises *every*
record belonging to an instrument into one array before applying the
predicate, and mongodb caps a single document's $lookup output at 100 MiB
(error 4568). The limit cannot be raised and allowDiskUse does not apply
to it. Because the threshold is per-instrument, the busiest instrument
hits it first -- a core intake instrument administered at every visit
reaches 182,000 records at 5,000 subjects x 36 visits.
`useInstrumentVisualization` calls this endpoint on mount and it backs
both /datahub/$subjectId/table and /datahub/$subjectId/graph, so there is
no partial degradation and no workaround available to the user.
Query the child collection instead, which is bounded by the subject's own
records rather than the instrument's, and has no size ceiling.
Verified against a live instance with one instrument at 105.7 MiB:
main HTTP 500 (three times), "exceeds 104857600 bytes"
branch HTTP 200 in 77ms, 811 bytes
And below the ceiling, where main still answers, on the same 55.3 MiB
instrument:
main 1.66-1.83s
branch 0.04-0.10s
Fixes #1416
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfen9VQemZanJLXinJ6MMG
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a MongoDB/Prisma scaling failure in the API’s instrument info query (GET /v1/instruments/info?subjectId=) by avoiding a high-cardinality relation filter that Prisma compiles into a $lookup capable of exceeding MongoDB’s 100 MiB per-document limit. Instead of joining from Instrument to InstrumentRecord, it first queries the child collection to find relevant instrument IDs for the subject, then filters instruments by those IDs.
Changes:
- Replace the
records: { some: { subjectId } }Prisma relation filter with a two-step query:InstrumentRecorddistinct instrument IDs →Instrumentfiltered byid in [...]. - Add a private helper (
findInstrumentIdsBySubject) to encapsulate the distinct ID lookup. - Add unit tests guarding against reintroducing the relation filter and verifying the new query behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/api/src/instruments/instruments.service.ts | Reworks subject filtering to query InstrumentRecord for distinct instrument IDs, avoiding a MongoDB $lookup blowup. |
| apps/api/src/instruments/tests/instruments.service.spec.ts | Adds regression tests ensuring the subject filter is resolved via InstrumentRecord and that no record query runs when subjectId is absent. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| vi.spyOn(instrumentsService as any, 'instantiate').mockResolvedValue([]); | ||
| }); | ||
|
|
||
| it('should not query instruments at all when no subject is given', async () => { |
Comment on lines
+292
to
+296
| const records = await this.instrumentRecordModel.findMany({ | ||
| distinct: ['instrumentId'], | ||
| select: { instrumentId: true }, | ||
| where: { subjectId } | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1416.
GET /v1/instruments/info?subjectId=returns HTTP 500 — and the datahub subject view stops rendering entirely — once a single instrument accumulates roughly 182,000 records.Cause
InstrumentsService.findexpressed "instruments this subject has records for" as a prisma relation filter:On MongoDB, prisma compiles that into a
$lookupwhich materialises every record belonging to an instrument into a single array before applying the predicate. MongoDB caps one document's$lookupoutput at 100 MiB and aborts the whole aggregation past it (error 4568):The limit cannot be raised, and
allowDiskUsedoes not apply to it.Two things make this worse than the raw number suggests:
useInstrumentVisualizationcalls this endpoint on mount, and it backs both/datahub/$subjectId/tableand/datahub/$subjectId/graph, so both pages fail and the user has no workaround.The fix
Query the child collection instead of joining from the parent. The work is then bounded by the subject's records (tens) rather than the instrument's (hundreds of thousands), and there is no size ceiling:
Verified against a live instance
Real API, real MongoDB replica set, database provisioned through the app's own
POST /v1/setup, then one instrument pushed past the ceiling.Above the ceiling — busiest instrument at 105.7 MiB, identical database for both:
mainexceeds 104857600 bytesBelow the ceiling — same 55.3 MiB instrument, where
mainstill answers:mainSo this is not only an outage fix: even well under the limit the endpoint was taking ~1.7 seconds to return 811 bytes, and it now takes ~40 ms.
Tests
Three added to
instruments.service.spec.ts:InstrumentRecordand the instrument query contains norecordsrelation filter (this is the regression guard — it asserts the absence of the construct that fails);{ id: { in: [] } }rather than an unfiltered query.The existing
findInfotests are unchanged and still pass.Checks
tsc --noEmitonapps/api— cleaneslint src— cleanprettier --check— cleanvitest run(whole workspace) — 255 passed, 1 skipped, 1 failedThe one failure is
instrument-records.service.spec.ts > upload > should create records with pending set. This branch is cut frommain, so it still carries that pre-existing failure — #1422 fixes it, and the suite is fully green there.Notes
@@index([instrumentId])onInstrumentRecord(proposed in Performance: no indexes are ever created on any MongoDB collection #1406) is a useful mitigation for the old query but not a fix — it does not remove the 100 MiB ceiling. This change removes the ceiling from the picture entirely; the index still helps the new query.FilesService.findusesfiles: { every: ... }, which is bounded by files-per-record and is safe today; it is the only other instance.🤖 Generated with Claude Code
https://claude.ai/code/session_01Bfen9VQemZanJLXinJ6MMG