Summary
Options passed to client-side data-source calls — sort, limit, projection,
upsert — are silently ignored. A collection.find(filter, { sort, limit })
runs as an unbounded, unsorted find({}) and returns the entire collection
in natural order. Only the filter/query is honored. The same flattening
affects findOne, count, and the findOneAnd* family.
This is a wire-format mismatch between the official realm-web SDK and the
flowerbase data-source controller. It is present on current main.
Impact
find — intended "newest 100" feed query instead streams every document.
For a large collection (70k+ docs with embedded sub-objects, in our case) this
is effectively a hang on the client, and the relevant index is never used
because no sort/limit reaches the cursor.
findOne(…, { sort }) — the common "get the latest/earliest" pattern
returns an arbitrary natural-order document rather than the sorted one. This is
a correctness bug, not just performance.
count(…, { limit }) — a capped count returns the full count.
findOneAndUpdate(…, { upsert: true }) — upsert is dropped, so an
intended upsert silently no-ops when no document matches.
Root cause
The realm-web SDK flattens the options object before sending: it lifts
the option fields up to the top level of the service-call argument (and, for
find/findOne, renames projection → project). There is no options
object on the wire.
// realm-web: RemoteMongoDBCollection.find
find(filter = {}, options = {}) {
return this.functions.find({
database, collection,
query: filter,
project: options.projection, // top-level, renamed "project"
sort: options.sort, // top-level
limit: options.limit, // top-level
});
}
Resulting HTTP body for find:
{ "name": "find", "service": "mongodb-atlas",
"arguments": [ { "database": "...", "collection": "...",
"query": {}, "project": null, "sort": {"date":-1}, "limit": 100 } ] }
The data-source controller
(packages/flowerbase/src/features/functions/controller.ts) destructures only a
nested options object (and reads projection, not project):
const [{ database, collection, key, query, filter, update,
projection, options, returnNewDocument, document,
documents, operations, pipeline = [] }] = args;
Because the SDK never sends options, the controller passes options: undefined
into executeQuery. In executeQuery's find branch
(packages/flowerbase/src/features/functions/utils.ts), the
parsedOptions?.sort / parsedOptions?.skip / parsedOptions?.limit guards are
therefore all skipped, and the cursor is built and .toArray()-ed with no sort,
projection, or limit. The cursor plumbing itself is correct — the values are lost
one layer up, in the controller's destructuring, because they arrive at the top
level instead of inside options.
Affected methods
Every read/mutate method flattens its options the same way, so the controller
drops them across the family:
| SDK method |
Sent at top level |
Honored |
Dropped |
find |
query, project, sort, limit |
query |
sort, limit, project |
findOne |
query, project, sort |
query |
sort, project |
count |
query, limit |
query |
limit |
findOneAndUpdate |
filter, update, sort, projection, upsert, returnNewDocument |
filter, update, returnNewDocument¹ |
sort, projection, upsert |
findOneAndReplace |
filter, update, sort, projection, upsert, returnNewDocument |
filter, update, returnNewDocument¹ |
sort, projection, upsert |
findOneAndDelete |
filter, sort, projection |
filter |
sort, projection |
¹ returnNewDocument survives only because executeQuery special-cases it
(synthesizing { returnDocument: … } when options is absent). No other option
gets that treatment.
Note the SDK renames projection → project for find/findOne, but keeps
projection for the findOneAnd* mutators, so both spellings appear on the wire.
Steps to reproduce
// any collection with more than `limit` documents
const docs = await collection.find({}, { sort: { someDate: -1 }, limit: 10 });
// Expected: 10 documents, newest first
// Actual: every document in the collection, natural order
Environment
@flowerforce/flowerbase 1.10.0 (confirmed identical on current main)
realm-web client SDK
Possibly related
#54 (large concurrent find() memory/perf) — different cause, but unbounded
find results would compound it.
Summary
Options passed to client-side data-source calls —
sort,limit,projection,upsert— are silently ignored. Acollection.find(filter, { sort, limit })runs as an unbounded, unsorted
find({})and returns the entire collectionin natural order. Only the
filter/queryis honored. The same flatteningaffects
findOne,count, and thefindOneAnd*family.This is a wire-format mismatch between the official
realm-webSDK and theflowerbase data-source controller. It is present on current
main.Impact
find— intended "newest 100" feed query instead streams every document.For a large collection (70k+ docs with embedded sub-objects, in our case) this
is effectively a hang on the client, and the relevant index is never used
because no
sort/limitreaches the cursor.findOne(…, { sort })— the common "get the latest/earliest" patternreturns an arbitrary natural-order document rather than the sorted one. This is
a correctness bug, not just performance.
count(…, { limit })— a capped count returns the full count.findOneAndUpdate(…, { upsert: true })—upsertis dropped, so anintended upsert silently no-ops when no document matches.
Root cause
The
realm-webSDK flattens theoptionsobject before sending: it liftsthe option fields up to the top level of the service-call argument (and, for
find/findOne, renamesprojection→project). There is nooptionsobject on the wire.
Resulting HTTP body for
find:{ "name": "find", "service": "mongodb-atlas", "arguments": [ { "database": "...", "collection": "...", "query": {}, "project": null, "sort": {"date":-1}, "limit": 100 } ] }The data-source controller
(
packages/flowerbase/src/features/functions/controller.ts) destructures only anested
optionsobject (and readsprojection, notproject):Because the SDK never sends
options, the controller passesoptions: undefinedinto
executeQuery. InexecuteQuery'sfindbranch(
packages/flowerbase/src/features/functions/utils.ts), theparsedOptions?.sort/parsedOptions?.skip/parsedOptions?.limitguards aretherefore all skipped, and the cursor is built and
.toArray()-ed with no sort,projection, or limit. The cursor plumbing itself is correct — the values are lost
one layer up, in the controller's destructuring, because they arrive at the top
level instead of inside
options.Affected methods
Every read/mutate method flattens its options the same way, so the controller
drops them across the family:
findquery,project,sort,limitquerysort,limit,projectfindOnequery,project,sortquerysort,projectcountquery,limitquerylimitfindOneAndUpdatefilter,update,sort,projection,upsert,returnNewDocumentfilter,update,returnNewDocument¹sort,projection,upsertfindOneAndReplacefilter,update,sort,projection,upsert,returnNewDocumentfilter,update,returnNewDocument¹sort,projection,upsertfindOneAndDeletefilter,sort,projectionfiltersort,projection¹
returnNewDocumentsurvives only becauseexecuteQueryspecial-cases it(synthesizing
{ returnDocument: … }whenoptionsis absent). No other optiongets that treatment.
Note the SDK renames
projection→projectforfind/findOne, but keepsprojectionfor thefindOneAnd*mutators, so both spellings appear on the wire.Steps to reproduce
Environment
@flowerforce/flowerbase1.10.0 (confirmed identical on currentmain)realm-webclient SDKPossibly related
#54 (large concurrent
find()memory/perf) — different cause, but unboundedfindresults would compound it.