MongoDB Provider
Document-database support for dbportal, built on the official
mongodbNode.js driver. This document is the single reference point for the MongoDB provider: design, architecture, usage, and tests. MongoDB is a document database — not relational — so, like the Redis provider, it extendsBaseDatabaseProviderdirectly (notSQLBaseProvider) and speaks a JSON query language, not SQL.
| Status | ✅ Implemented & shipped |
| Database type id | mongodb |
| Family | Document |
| Driver | mongodb (official Node.js driver) |
| Server floor | MongoDB 4.4 — the driver removed 4.2 support in 7.6.0 and now throws when it connects to a server of 4.2 or older, so this is a hard floor rather than a degraded surface (release notes) |
| Query language | json (MQL — Mongo Query Language as a JSON object) |
| Default port | 27017 |
| Connection pooling | Yes — the driver’s built-in MongoClient pool |
| Connection string | ✅ Supported and used directly (mongodb:// / mongodb+srv://) |
| Transactions | ❌ no explicit begin/commit/rollback API |
| Query cancellation | ❌ no cancelQuery (operations can be killed via maintenance killOp) |
| SSL | Yes — connection.ssl → tls + Node’s ca/cert/key (§4.1) |
| Source | src/lib/db/providers/document/mongodb.ts |
| Tests | tests/integration/db/mongodb-provider.test.ts |
1. Overview
Section titled “1. Overview”MongoDB stores schemaless BSON documents in collections. It maps onto the DatabaseProvider
interface by convention (like Redis), relabelling the generic UI for document semantics and
accepting queries as JSON rather than SQL.
Concept mapping
Section titled “Concept mapping”DatabaseProvider slot |
MongoDB realisation |
|---|---|
| “Table” (the relation kind) | A collection |
| “Row” | A document |
| Columns | Inferred field types from a 100-document sample |
query(sql) |
A JSON MQL command ({collection, operation, …}) |
| Foreign keys | none (MongoDB has no FKs) |
| Maintenance | validate / compact / dbCheck (mapped to analyze/vacuum/check) |
| Monitoring | serverStatus, dbStats, currentOp, $indexStats, the profiler |
Unlike Redis (a key-value store), MongoDB is genuinely query-rich: find, aggregate, count,
distinct, and the full set of write operations are supported.
2. Architecture
Section titled “2. Architecture”DatabaseProvider (interface) → BaseDatabaseProvider → MongoDBProviderMongoDBProvider extends BaseDatabaseProvider directly and overrides getCapabilities(),
getLabels(), and prepareQuery(). It inherits the base’s getMonitoringData() orchestration and
state/instrumentation helpers (see Redis doc §2.3 for the shared base behaviour).
Registration
Section titled “Registration”Loaded on demand by createDatabaseProvider() (factory.ts):
case 'mongodb': { const { MongoDBProvider } = await import('./providers/document/mongodb'); return new MongoDBProvider(connection, options);}3. Design decisions
Section titled “3. Design decisions”3.1 JSON / MQL query format
Section titled “3.1 JSON / MQL query format”query() (mongodb.ts) accepts a JSON object,
parsed by parseQuery() (mongodb.ts), which
requires collection and operation:
{ "collection": "users", "operation": "find", "filter": {"age": {"$gt": 18}}, "options": {"limit": 10} }{ "collection": "orders", "operation": "aggregate", "pipeline": [{"$group": {"_id": "$status", "count": {"$sum": 1}}}] }{ "collection": "products", "operation": "distinct", "field": "category", "filter": {"active": true} }{ "collection": "users", "operation": "insertOne", "documents": [{"name": "John"}] }distinct is the one operation with a key of its own: field, the driver’s own parameter name, and
it is required. The example above answers one row per category, shaped { "category": <value> }.
A missing or non-string field is a QueryError naming the key it wanted — it used to read the
field from the first key of options.projection and fall back to _id, so
{"operation": "distinct", "field": "category"} answered 120 rows of _id (measured 2026-08-22 on
mongo:latest, 120 products in five categories). options.projection is not an alias for it.
Supported operations: find, findOne, aggregate, count, distinct, insertOne, insertMany,
updateOne, updateMany, deleteOne, deleteMany. See the
API_DOCS.md MongoDB Query Format section (under POST /api/db/query) and
CLAUDE.md for the request shape.
Since S8 the execution confirmation gate reads this same shape:
src/lib/db/destructive-commands.ts names deleteOne, deleteMany, updateOne, updateMany and
the top-level $out / $merge aggregate stages as the destructive operations here - so each of
them asks before it runs, exactly as a DELETE FROM does on a SQL engine, and a payload the reader
cannot read as a document with a string operation (mongosh syntax such as
db.users.deleteMany({}), a half-typed object) asks as well rather than staying silent.
3.2 BSON serialization for the grid
Section titled “3.2 BSON serialization for the grid”serializeDocument() (mongodb.ts) recursively
normalises BSON types so documents render in the JSON grid: ObjectId → string, Decimal128 →
string, Date → ISO-8601, Binary → <Binary: N bytes> (placeholder, not the raw bytes), and
nested objects/arrays are walked recursively. Only these types are special-cased — other BSON
types (Long, Timestamp, UUID, RegExp, Code, DBRef) fall through as generic objects and
may render poorly (Known limitations).
3.3 Sampling-based schema inference, nested to three levels
Section titled “3.3 Sampling-based schema inference, nested to three levels”MongoDB has no fixed schema, so the object surface (mongodb.ts)
infers one: it lists collections (skipping system.*, capped at 200), and for each samples the
first 100 documents to derive field types (mongodb.ts).
Caveats baked into this approach:
- Fields absent from the sample (or appearing only in unsampled documents) won’t show.
- Subdocuments are expanded into dotted paths, to
MAX_NESTED_FIELD_DEPTH = 3counting the top level as 1 — soshipping,shipping.cityandshipping.geo.latare all listed, andshipping.geo.deep.tooFaris not. The container at the boundary is still named, so a reader can see that the nesting continues.shipping.cityis a field name in MQL, and a schema that stopped atshipping: objectdid not name it: a plan run on 2026-08-22 grouped by$shipping.region, a path the database does not have, and MongoDB answers that with one null group rather than an error — so the plan read as runnable and was silently wrong. - Arrays are named and left closed.
items.skuaddresses one value per array entry, so it does not mean on an array what the same syntax means on a subdocument; listing it in a flat field list would invite exactly that confusion. Date/ObjectId/Binary/Decimal128 are scalars here and are never descended into. - The field list is capped at
MAX_INFERRED_FIELDS = 200per collection, applied after the sort, so what survives is a deterministic prefix and_idalways survives. Nesting multiplies: 60 subdocuments of 10 fields each is 661 rows in the schema tree and 661 lines in an agent run’s context window, for one collection. - A field with multiple observed types is reported as
mixed(a|b)._idis marked primary.
3.4 find is capped at 100; aggregate is not
Section titled “3.4 find is capped at 100; aggregate is not”A find with no explicit options.limit is capped at 100 documents
(mongodb.ts). aggregate passes none of
options to the cursor (no limit/skip) and has no default cap, so a pipeline without a
$limit stage can return an unbounded result set.
prepareQuery() does not modify the query (it injects no limit — the JSON is passed through
unchanged), but it is not a true no-op: it returns limit: options.limit || 100, and the
/api/db/query route uses that returned limit/wasLimited for pagination metadata
(hasMore = rows.length === prepared.limit). The unlimited option is not honoured — see
Known limitations.
4. Connection
Section titled “4. Connection”connectionString is used directly (this is a genuine connection-string provider, unlike
SQL Server). buildConnectionString() (mongodb.ts)
returns config.connectionString if present, else assembles
mongodb://<user>:<password>@<host>:<port>/<database>[?authSource=<authSource>] (credentials and
the auth database are URL-encoded; the <user>:<password>@ segment is omitted when no credentials
are set, and the query string when no authSource is).
authSource is the database the credentials live in, and it is not always the one being opened.
MongoDB creates users inside a database, and the driver checks them against whichever database the
URI names when nothing says otherwise — so the ordinary deployment, users in admin and data
elsewhere, could not be reached through the discrete fields at all: it failed as a credentials
error, which is what it looks like and is not what it is. Leave the field empty when the user
was created in the database being opened. A pasted connectionString is used verbatim and carries
its own ?authSource=, so the form offers no separate input in that mode.
// Connection string (SRV or standard)const a = { id: 'mg-1', name: 'App', type: 'mongodb', connectionString: 'mongodb+srv://user:pass@cluster.example.net/app', createdAt: new Date() };
// Discrete fieldsconst b = { id: 'mg-1', name: 'App', type: 'mongodb', host: 'localhost', port: 27017, database: 'app', user: 'admin', password: 'secret', createdAt: new Date() };
// Discrete fields, user created in `admin` — the ordinary deploymentconst c = { id: 'mg-1', name: 'App', type: 'mongodb', host: 'localhost', port: 27017, database: 'shop', user: 'app', password: 'secret', authSource: 'admin', createdAt: new Date() };validate() (mongodb.ts) requires either a
connectionString or both host and database. connect() builds a MongoClient whose built-in
pool is configured from ProviderOptions.pool:
MongoClient option |
Source |
|---|---|
maxPoolSize |
pool.max |
minPoolSize |
pool.min |
maxIdleTimeMS |
pool.idleTimeout |
connectTimeoutMS |
pool.acquireTimeout |
serverSelectionTimeoutMS |
pool.acquireTimeout |
The database name comes from config.database, else it is parsed out of the connection string, else
defaults to test. After connecting, a { ping: 1 } command validates the connection.
4.1 SSL / TLS
Section titled “4.1 SSL / TLS”buildTLSOptions() (mongodb.ts) maps
connection.ssl onto the driver’s TLS options. tls, ca, cert, key and rejectUnauthorized
are all on the driver’s own allow-list (LEGAL_TLS_SOCKET_OPTIONS in mongodb/lib/cmap/connect.js)
and reach tls.connect under Node’s names, so the material maps exactly as it does for PostgreSQL,
MySQL and Couchbase:
ssl.mode |
Options added |
|---|---|
absent / disable |
none — the client is built as before |
require |
tls: true, rejectUnauthorized: false |
verify-system |
tls: true, rejectUnauthorized: true, and no ca |
verify-ca / verify-full |
tls: true, rejectUnauthorized: true |
caCert / clientCert / clientKey become ca / cert / key when set, each independently — a
cluster can demand mutual TLS while presenting a self-signed certificate itself. An explicit
ssl.rejectUnauthorized always wins over the mode. require does not check the chain because a
self-hosted replica set presents a self-signed certificate by default; verify-system is the same
handshake with verification on and nothing to paste, which is what an Atlas cluster needs (its
certificate is signed by a public root the runtime already trusts). The driver exposes no separate
host-name check, so verify-ca and verify-full build the same options object.
tls=true in a pasted URI (D26)
Section titled “tls=true in a pasted URI (D26)”The paste box now reads the URI’s own TLS options, and maps them by the rule stated in
readBooleanTLS — a boolean TLS spelling lands on the mode that matches what the driver does with it:
| In the pasted URI | SSL Mode set on the form |
|---|---|
tls=true / ssl=true |
verify-system — the driver enables TLS with chain verification |
tls=false / ssl=false |
disable |
mongodb+srv:// with no TLS parameter |
verify-system — SRV implies TLS in the driver itself |
tlsInsecure=true / tlsAllowInvalidCertificates=true alongside TLS |
require — both turn rejectUnauthorized off |
a non-boolean value (tls=maybe) |
nothing; the paste banner quotes the parameter |
tls=true was deliberately ignored before this: the only non-disable mode that needed no PEM
was require, i.e. rejectUnauthorized: false, and because the options object is a second channel
the driver prefers over the URI, setting it would have stopped an Atlas certificate being verified.
Leaving it unset had its own cost — the SSL panel read disable for a connection that was in fact
encrypted. verify-system removes the trade: the form now says what the URI says.
tlsAllowInvalidHostnames is not in the relaxing set, because this provider never sends
checkServerIdentity, so the URI’s own relaxation survives next to a verifying mode.
Measured against a TLS-only server on 2026-08-23 (mongo:latest --tlsMode requireTLS): disable is
refused - the server logs “The server is configured to only allow SSL connections” - and require
connects in 20ms, with “Ingress TLS handshake complete” on the server side. Both arms matter, since
before the mode reached the driver require failed the same way disable does.
Unlike
authSource, this is applied alongside a pastedconnectionString. The URI is returned verbatim, so atls=cannot be appended to it, but the options object is a second channel the driver reads — and the connection dialog shows the SSL panel in connection-string mode too, so a selection made there has to mean something.
5. Query interface
Section titled “5. Query interface”query(jsonString) parses the MQL object and dispatches on operation
(mongodb.ts). Reads (find/findOne/
aggregate/count/distinct) return documents; writes return an acknowledgement summary
(insertedId/modifiedCount/deletedCount, …). rowCount = rows.length || affectedCount, and
every returned document passes through serializeDocument(). There is no prepareQuery limit
injection, no transactions, and no cancelQuery. EXPLAIN is not supported
(supportsExplain: false).
options handling differs per operation (a real source of surprise — see
Known limitations):
findhonoursprojection/sort/skip/limit.findOnehonours onlyprojection— asort/skip/limitis silently ignored (so{ "operation": "findOne", "options": { "sort": { "_id": -1 } } }does not return the latest document).aggregateignoresoptionsentirely (bound it with a$limitstage in the pipeline).distinctignoresoptionsentirely and takes its field from the top-levelfieldkey, e.g.{ "collection": "users", "operation": "distinct", "field": "country" }returns distinctcountryvalues (output shape{ "country": <value> }). The key is required: a missing or non-string one is aQueryError, never a silent_id.
6. Schema introspection
Section titled “6. Schema introspection”the object surface answers one object per collection:
| Data | Source |
|---|---|
| Collections | listCollections() (skip system.*, cap 200) — views included, see below |
| Row count | estimatedDocumentCount() — not asked of a view; absent there |
| Size | collStats command (size) — not asked of a view; absent there |
| Columns | inferred from a 100-document sample (§3.3), on a view exactly as on a collection |
| Indexes | collection.indexes() (unique flag, key fields) — not asked of a view; [] there |
| Foreign keys | always [] — MongoDB has none to declare, which the provider states as declaresForeignKeys: false (§9) rather than leaving a reader to guess whether the read simply found none |
Views are listed, and are asked less
Section titled “Views are listed, and are asked less”listCollections() returns views alongside collections, and MongoDB rejects count, listIndexes
and collStats on a view with CommandNotSupportedOnView (code 166). Those three calls used to be
unguarded, so one view in the database aborted the whole schema read — the user lost every
collection, not just the view.
The fix guards them on collInfo.type === "view", which the server has already reported, rather than
filtering views out of the listing. A view is an object the user created and expects to see, and its
fields are readable by the same document sample every collection gets; hiding it would answer “your
view does not exist” in order to keep three commands quiet. What a view genuinely cannot answer is
left absent rather than defaulted: no rowCount (a view holds no documents of its own, and 0
would read as “empty”) and no size, with indexes: [] because the indexes its query uses belong to
the collection underneath it.
The object surface (#789)
Section titled “The object surface (#789)”The deleted flat reading answered one flat collection list, for the connected database only. The object
surface answers a lazy, per-kind tree across every database the connection can see, through four
methods: listContainers, countObjects, listObjects and describeObject(path, kind).
MongoDB is the first non-SQL engine in #789 to get one. The four methods and the model are the shared ones; what is different is that the catalog is a command rather than a query, which moves the seam the contract’s hardest rule guards — see below.
Everything in this section was measured on 2026-09-11 against a live MongoDB 8.3.9 holding the
committed fixture, docker/mongodb-init/01-object-fixture.js.
The recipe that applies it is in §11, so every claim below can be re-measured rather
than trusted.
The declaration
Section titled “The declaration”One container level, the database, and two kinds:
| Kind | Role | Source | Path |
|---|---|---|---|
collection |
relation, acceptsRowWrites |
listCollections, every type that is not view |
[database, collection] |
view |
relation | listCollections, type: "view" |
[database, view] |
acceptsRowWrites on collection is the per-kind half and is deliberately not conjoined with
this provider’s engine-wide supportsInlineRowEdit: false (§9). That flag
is about the results grid’s UPDATE … SET, which has no MongoDB spelling; an import into a
collection is an ordinary insertMany. A view carries no such declaration: the server reports
info.readOnly: true on every one, on the same call that classifies it.
What is not declared, and why each absence is a measurement
Section titled “What is not declared, and why each absence is a measurement”- No
indexkind. An index name is unique per collection, not per database: creatingby_thingonapp.customersand again onapp.ordersboth succeed, and creating it twice on one collection is refused with An existing index has the same name as the requested index. So the catalog does not model an index as a first-class container-level object, which is the test standing ruling 4 sets. An index appears indescribeObject’s output, where it is. The fixture creates that duplicate name on purpose, so the reading stays refutable. (sqlite, Cassandra and Couchbase are the three engines that do declare the kind, and each earns it the same way.) - No routine kind of any shape.
$function,$accumulator,$whereandsystem.jsare all deprecated as of MongoDB 8.0,mapReducesince 5.0, anddb.evalwas removed in 4.2. Atlas Triggers and Atlas Functions are an Atlas control-plane feature, not a server one: the wire protocol this provider speaks cannot reach them at all. - No kind for an on-demand materialized view.
$mergeand$outwrite an ordinary collection carrying no server-side marker of where it came from, so there is nothing to list and nothing that could be listed consistently. - No separate
timeserieskind, and that one is a decision rather than an absence in the engine. See the next subsection.
A kind an engine does not have is simply absent, never declared and counted zero: a declared kind draws a folder, and a folder for something the engine cannot have is a lie the zero badge makes look like a fact.
A time series collection is a collection, and the classifier says so the careful way
Section titled “A time series collection is a collection, and the classifier says so the careful way”listCollections answers a third value for type: beside collection and view there is
timeseries, reported for a collection created with a timeseries option. Measured; the fixture
creates app.readings precisely so it is in the data.
The classifier is therefore written “view versus everything else” and never type === "collection". The wrong spelling loses such an object from the count and the listing at once, so
the count would still equal the listing while an object a person created was invisible in the tree —
the absence that passes every gate. A test names that case, and the fixture is what makes the test
non-vacuous.
It is counted and listed as a collection rather than under a folder of its own because everything
the tree does with a collection it does with this one: it holds documents, find reads them,
listIndexes answers real indexes (sensor_1_ts_1 on the fixture’s) and collStats answers a size
— all measured. MongoDB’s own show collections lists it beside the others. A third folder would
divide a person’s collections by a storage detail.
Which databases and which namespaces are excluded
Section titled “Which databases and which namespaces are excluded”The server’s own three databases — admin, config and local — are excluded by exact name,
never by prefix. Measured: databases named configstore, localx and adminx are all created
without complaint, so a prefix rule would hide a database a person made. The fixture creates
configstore so that spelling stays refuted rather than merely unattractive.
Inside a database, the reserved namespace prefix is system. with the dot. Measured:
createCollection("system.mine") is refused with not authorized on app to execute command, while
systemetrics is created without complaint. The fixture holds systemetrics, so a rule written on
the letters system without the dot fails a test by name. The two internal namespaces the fixture’s
own listing contains are system.views (created the moment a view is) and
system.buckets.readings (the bucket collection behind the time series one).
listDatabases is sent as { listDatabases: 1, nameOnly: true, authorizedDatabases: true }. The
flag is load-bearing rather than tidy: the server’s default for it depends on whether the connecting
role holds the cluster-wide listDatabases action, so a role granted only read on one database
would otherwise be at the mercy of a default this provider never stated. Measured both ways — with
the flag a root role still sees every database and a read-on-one role sees exactly its own.
Where the count and the listing could drift
Section titled “Where the count and the listing could drift”The rule is that the listing must contain exactly what the count counted, and four SQL providers
in this epic failed it on a first pass by letting a second WHERE clause drift from the first.
There is no WHERE clause here, so the seam moves to the classifier. countObjects tallies
listCollections rows by kind and listObjects filters the same rows by kind; written twice, those
two decisions would be free to disagree. mongoObjectKind() is the one place the decision is made
and the only reader of both the type field and the internal-namespace rule, and all of
countObjects, listObjects and describeObject route through it over rows from one shared
listCollections read. There is nothing left for the two answers to differ in.
countObjects seeds every declared kind at { count: 0 } before reading rows, so a database holding
no views still draws a Views folder with a 0 badge instead of losing it. A refused read is a third
state, not a zero: a role with read on one database answers listCollections on any other with
not authorized on <db> to execute command { listCollections: 1 … }, and that sentence is carried
to the tree verbatim as { unavailable } rather than reported as an empty database. Because both
kinds come from one command, a refusal is one fact about the whole database rather than the
per-kind privilege it is on an engine with one catalog table per kind.
What describeObject answers
Section titled “What describeObject answers”The kind decides, and nothing reads the name to work out what it is holding: the lookup requires
the catalog row to classify as the kind that was asked for, so describeObject(["app", "active_customers"], "collection") is a miss rather than a view described as a collection.
- Columns are inferred from a 100-document sample, exactly as the deleted flat reading inferred them and with the same bound (§3.3), because MongoDB stores no schema to read. This works on a view exactly as it works on a collection, which is why a view is worth listing at all.
- Indexes come from
listIndexesfor a collection and are always[]for a view. That is measured, not defensive:listIndexeson a view is refused withCommandNotSupportedOnView(code 166), and the indexes its pipeline actually uses belong to the collection underneath it, so claiming them here would misattribute them — the same reading §6 already applies to the object surface. foreignKeysis always[], because MongoDB has no foreign key constraint at all. The same measurement is behinddeclaresForeignKeys: false.
A listed object carries no rowCount and no sizeBytes, and that is a bound rather than a gap:
either would need collStats or estimatedDocumentCount per collection, one round trip each,
and this folder is the one a person opens to see what is there. describeObject is where a single
object’s detail is paid for.
A view’s options.viewOn and options.pipeline arrive on the same listCollections call that
classified it, so Phase 2’s Source tab needs no second read. Phase 1 renders neither.
What describeObjects answers, and the one half this engine cannot bulk-read
Section titled “What describeObjects answers, and the one half this engine cannot bulk-read”describeObjects(container, kind, limit?) is the bulk column read (#789): columns and indexes for
every object of one kind in one database. The two halves of an ObjectDetail do not have the
same answer here, and both answers are measurements, re-measured for this section on a
container this task created and removed, mongo:latest, which reports version 8.2.12.
The columns can be bulk-read, in one aggregate per chunk. There is no catalog of fields to
read, so the single read samples documents; $unionWith samples every collection in ONE pipeline,
one arm each, each arm bounded by the same 100-document limit the single read uses and tagging its
rows with the collection they came from (the chain answers one flat stream, so an untagged arm
would be unattributable). It works on a view and on a time series collection as readily as
on an ordinary one, measured against the committed fixture.
The chain is chunked at 100 collections, because one arm is one pipeline stage and MongoDB
bounds a pipeline’s stage count (internalPipelineLengthLimit, 1,000 by default): a 5,000-
collection folder would be refused outright as one chain. Round trips grow as the folder divided by
the chunk. The number is measured rather than picked - over a 200-collection database, one 200-arm
aggregate took 81 ms, two 100-arm ones 67 ms and four 50-arm ones 55 ms - and 100 keeps a tenfold
margin under the server’s own ceiling.
The indexes cannot be bulk-read at all, and three separate measurements say so.
$listCatalogis the only server-side bulk index listing, and its collectionless form must run againstadminwith{aggregate: 1}. A role holdingreadon one database - whichlistCollections,listIndexesand every other read in this surface accept - is refused it:not authorized on admin to execute command { aggregate: 1, pipeline: [ { $listCatalog: {} } ... ] }. Using it would make this one method need a cluster privilege the other four do not.$listCatalogalso answers the wrong indexes for a time series collection. It reports two entries forreadings:app.readings, the namespace a person addresses, carrying no indexes at all, andapp.system.buckets.readings, carryingsensor_1_ts_1under the bucket collection’s rewritten keys (meta,control.min.ts,control.max.ts).listIndexeson the collection itself answers the index a person created,sensor_1_ts_1oversensorandts. The single read answers that one, so the bulk read must too.$indexStatsneeds a privilege this surface does not have. It CAN be folded into a$unionWithsub-pipeline - measured, the server accepts it and answers the arm’s index rows, which refutes the$indexStats is only valid as the first stage in a pipelinereading of it - but a role holdingreadon one database is refused it both directly and inside the chain:not authorized on app to execute command { aggregate: "customers", pipeline: [ { $indexStats: {} } ] }. The same role runslistIndexeson the same collection without complaint.
So the index reads are one per described object, issued in parallel and bounded by the caller’s
limit. This is the one place in the seventeen providers where a per-object read survives, and it
is still not the N+1 the inventory route removed: that was up to 5,000 sequential round trips
over a whole listing. Measured over 200 collections:
| Shape | Round trips | Time |
|---|---|---|
Sequential fan-out (a loop over describeObject) |
400 | 171 ms |
| Parallel fan-out | 400 | 67 ms |
$unionWith samples plus parallel listIndexes (this) |
202 | 63 ms |
Through the provider itself against a bench database of 200 collections holding 120 documents
each: 108 ms for one describeObjects against 422 ms for the 200 describeObject calls it
replaces.
A view folder pays none of the index half: a view has no indexes of its own, listIndexes on
one is refused with code 166, and nothing is sent. So a view folder’s bulk read really is constant
per folder.
No kind here answers an empty batch for want of columns. The reference implementation’s fourth guard covers a routine, a trigger or a sequence, and MongoDB declares none. An empty folder still sends nothing, and no guard is written for it: with no names there are no chunks and no index reads, so an early return would be a branch no data can reach - a mutation deleting it failed nothing, which is what dead means.
One mapper, shared with the single read. objectDetailFrom() builds both. Verified live: for
every object of every kind in app, oddnames and configstore, the bulk read’s detail is
byte-identical to describeObject() for the same path.
The bound is the caller’s, and there is no limit + 1. That extra row exists to tell a
saturated read from an exact one without a second count, and it is unnecessary here:
listCollections answers the whole catalog in one command, so the target set is COMPLETE before
anything is cut and the comparison is exact. The driver’s cursor could not be bounded anyway -
listCollections takes no limit - so the cut is applied in code after the shared sort, which makes
a bounded read’s membership comparePaths’ rather than the server’s. The bound reaches the
expensive half: only the objects that will be returned are sampled and only their indexes are read.
the object surface’s silent .slice(0, 200) is not carried here. That is the reference’s first “do
not copy”: an unreported bound is the defect truncated exists to prevent.
Paths are derived, never indexed positionally
Section titled “Paths are derived, never indexed positionally”The database segment comes from the declared ContainerLevelSpec whose id is schema, the object’s
own name is the last segment, and the expected depth comes from containerDepth(). MongoDB declares
one level, so path[0] and container.length !== 1 are behaviour-identical here and silently wrong
on the two-level engines. The suite pins it anyway, by swapping a two-level declaration in through
getCapabilities and driving it all the way to the database name the driver was bound with,
rather than to a refusal.
The one place a path is constructed rather than read is listContainers, which builds [name].
Both listings are sorted by the path’s segments, never by JSON.stringify(path). That spelling
sorts by the escape sequence rather than by the name, and a MongoDB collection name may hold both a
quote and a backslash: only the null byte and $ are refused, measured. The fixture’s oddnames
database holds x"a, x-a and x\a for exactly that reason - by code point they sort x"a (0x22),
x-a (0x2D), x\a (0x5C), and by JSON.stringify they sort x-a, x"a, x\a. The server’s own
order for them is the JSON one, so a provider sorting the wrong way would pass by inheriting it.
listDatabases came back alphabetical in both live measurements and listCollections came back in
two different orders across two fresh containers holding the same fixture, so ordering is the
provider’s own guarantee either way rather than something inherited from the server.
7. Monitoring & health
Section titled “7. Monitoring & health”Rich, from admin().serverStatus(), db.stats(), currentOp, $indexStats, and the profiler.
Every method is wrapped in try/catch. Degradation reports the absence rather than filling it in — see
§7.1.
| Method | Source | Notes |
|---|---|---|
getHealth() |
serverStatus, dbStats, currentOp, system.profile |
connections (omitted, never 0, when the server publishes none — §7.2), data size ("N/A", never "0 B", when db.stats() answers without dataSize — §7.3), WiredTiger cache-hit % ("N/A" when unmeasurable, §7.1), current ops; slow queries need the profiler (placeholder row if disabled) |
getOverview() |
serverStatus, buildInfo, dbStats, listCollections |
version, uptime, connections (omitted, never 0, on the same two paths as getHealth() — §7.2), database size (databaseSizeBytes omitted, never 0, on those same two paths — §7.3), collection/index counts. maxConnections is connections.current + connections.available, or 0 — the repo’s spelling of no limit published — when the server publishes no headroom |
getPerformanceMetrics() |
serverStatus (WiredTiger + opcounters) |
cache-hit %, ops/sec (query+insert+update+delete opcounters ÷ uptime — total operations, not just queries), buffer-pool % (cache bytes), deadlocks: 0. Every field is optional: each one is present only if its reading was, and a failed serverStatus reports {} (§7.1) |
getSlowQueries() |
system.profile |
per-op time/returned; [] if the profiler isn’t enabled (db.setProfilingLevel(1)); sorted by millis (slowest) — note getHealth()’s slow-query block instead sorts by ts (most recent) and emits a placeholder row when disabled |
getActiveSessions() |
currentOp |
opid, ns, lock waits, duration — ⚠️ the user field is populated from op.client (the client host:port), not an authenticated user |
getTableStats() |
collStats per collection |
row count + data/index/total sizes, totalIndexSize carried as the byte figure indexSizeBytes and not only as formatted text |
getIndexStats() |
$indexStats + indexes() |
real scans (accesses.ops); indexSize N/A; indexType only distinguishes text vs btree — hashed/2dsphere/2d/wildcard/clustered are all mislabelled btree |
getStorageStats() |
dbStats + WiredTiger |
Data / Indexes / Storage / WiredTiger cache (with usage %) |
7.1 What the panel shows when the cache cannot be measured
Section titled “7.1 What the panel shows when the cache cannot be measured”The cache hit ratio is computed from serverStatus.wiredTiger.cache — pages read into cache over
pages requested from the cache. Three things can make that unmeasurable, and none of them is a
number:
- the deployment publishes no
wiredTigersection at all (mongos, the in-memory storage engine, the wire-compatible services); - the section is there but
pages requested from the cacheis0, on a server that has served nothing yet — no hits and no misses, which is not a perfect hit rate; serverStatusfails outright, which is what an unprivileged user gets (clusterMonitoris the role it wants).
In all three the field is omitted from getPerformanceMetrics() and getHealth() reports the
string "N/A"; the Overview and Performance tabs then render Cache Hit as N/A beside Not
measured, and the card border stays neutral instead of being rated. bufferPoolUsage follows the
same rule for the same section. A measured 0 — a genuinely cold cache — is kept and rendered as
0.0%, because it is a fact the server reported.
This replaces a hardcoded cacheHitRatio: 99 that both the no-wiredTiger path and the failed-
serverStatus path used to return: a figure the provider invented, indistinguishable at the panel
from a measurement (the rule #424 exists to
enforce, and #452 built the unavailable
rendering for).
7.2 A connection count nobody published is absent, not zero
Section titled “7.2 A connection count nobody published is absent, not zero”getHealth().activeConnections and getOverview().activeConnections both come from
serverStatus.connections.current, and there are two ordinary ways that reading does not happen:
- the deployment publishes no
connectionssection at all — an API-compatible service, or any deployment whoseserverStatusanswers without it. This is not §7.1’s set:connectionsis a network-layer field ofserverStatus, not a storage-engine sub-document likewiredTiger, so the reason that set omitswiredTigerdoes not carry here, and which deployments omitconnectionsis not measured in this repo. TheserverStatusmanual page offers no guarantee to measure it against either: it notes that “the output fields vary depending on the version of MongoDB, underlying operating system platform, the storage engine, and the kind of node, includingmongos,mongodor replica set member”, and describesconnectionsonly as “a document that reports on the status of the connections” — it promises no top-level field,connectionsincluded, on every deployment. So the provider treats aserverStatusthat answers without the section as an ordinary answer, without ruling on which deployments answer that way; serverStatus(ordb.stats(), orcurrentOp, orbuildInfo) fails outright, which is what a user withoutclusterMonitorgets, and the whole read then falls into its outer catch.
HealthInfo.activeConnections and DatabaseOverview.activeConnections are both optional for
exactly this case, so in all four paths the key is now omitted — absent from the object, absent
from the POST /api/db/health body, absent from the monitoring payload, and the admin fleet-health
row drops its N conn figure rather than printing 0 conn
(src/components/admin/tabs/OverviewTab.tsx).
Every one of them used to answer 0: connections?.current || 0 on both success paths and a literal
activeConnections: 0 in both outer catches. On the getHealth() side that reached the model: the
agent’s curated health reading is getHealth() (method: "getHealth" in
src/lib/agent/tools.ts, which projects this key with ?? null), so
a MongoDB the caller could not query at all arrived as a measured “no connections open”.
getOverview()’s count does not reach the model - nothing under src/lib/agent reads
getOverview() - and its readers are the monitoring Connections card, that card’s trend chart,
and the connection-threshold rating that colours it. On the Overview tab the 0 was not merely a
blank in disguise: it printed as the figure 0 on the Connections card, and — because that tab
keeps a history — each refresh added a real 0 point to the connection sparkline, whose flatMap
drops absent samples and plots present ones. With the key absent the card reads N/A above not
published and the sample is dropped from the trend
(src/components/monitoring/tabs/OverviewTab.tsx).
No percentage was ever involved on these paths: whenever connections.current is unmeasurable
maxConnections is 0 too, and the card only computes a share when a limit is published.
A server that really has 0 open connections keeps the 0 — it is a reading, and the absence is
spelled measuredNumber(...) plus a conditional spread, never || undefined and never || 0. The
|| 0 form was in fact two defects on one line: it invented a figure where none was published and
flattened a genuinely idle server’s measured 0 into the same value, so the two are
indistinguishable downstream.
Both outer catches still resolve rather than rethrowing, but for different reasons, and neither reason licenses naming a figure:
getHealth()resolves becausePOST /api/db/healthserialises whatever it resolves with andPOST /api/admin/fleet-healthreadshealthyfrom a read that returned, so rethrowing would report a server that is up as an error;getOverview()resolves becausegetMonitoringData()(src/lib/db/base-provider.ts) reads the panel throughPromise.allSettled, so a rethrow would replace the whole overview with anerrors.overviewentry instead of the"MongoDB Unknown"/"N/A"placeholders the tab renders. That is a legitimate alternative rather than a bug, and it is left alone here: it is a cross-provider decision, since every provider’sgetOverview()degrades the same way.
(getHealth()’s slowQueries: [{ query: "Error fetching health info" }] placeholder is the same
class of fabrication one size down and is still there; it is tracked separately, because removing it
needs HealthInfo.slowQueries to become optional across all 17 type-ids.)
7.3 A database size nobody published is absent, not 0 B
Section titled “7.3 A database size nobody published is absent, not 0 B”The byte figure travels the same two paths as the count above and had the same two spellings of a fabrication. Both outlived the round that fixed the count, one field over in the same object:
getOverview()’s outer catch returned a literaldatabaseSizeBytes: 0. Nothing was read on that path -serverStatus,db.stats(),buildInfoandlistCollectionsare all inside the sametry, so a user withoutclusterMonitorreaches it - andDatabaseOverview.databaseSizeBytesis optional precisely so that a provider with no byte figure can say so. The key is now omitted;- both success paths formatted
dbStats.dataSize || 0, which cannot tell a database that measures 0 bytes from adb.stats()that answered withoutdataSize. Both are nowmeasuredNumber(dbStats.dataSize): absent, the overview omitsdatabaseSizeBytesand reportsdatabaseSize: "N/A"; measured, a real0still formats as"0 B". MongoDB’s owndbStatsreference documentsdataSizeunconditionally - the only output fields it gates arefreeStorageSize,indexFreeStorageSizeandtotalFreeStorageSize, on the command’sfreeStorage: 1option - so this arm is not a deployment measured in this repo; it is the input|| 0could not distinguish, and the optional field exists to carry it.
What the absence buys is a whole panel.
StorageTab.tsx keys its entire breakdown off
overview?.databaseSizeBytes !== undefined. With the key present as 0 it treated the size as
known and drew the breakdown over a 0 B total: the Tables and Indexes cards at 0.0%, and
an Other (unattributed) row computed as totalSize - totalTableSize - totalIndexSize. That
remainder is the part a refused serverStatus makes visibly wrong rather than merely empty, because
the table read does not share the failure - getTableStats() goes through listCollections +
collStats, neither of which needs clusterMonitor - so its per-table byte figures arrive, both
known flags are true, and the row draws a negative byte count as a measurement. A 1 KB collection
with 512 B of indexes renders the literal string -1536 B (measured 2026-08-27 against the cascade
itself): the tab formats bytes with its own local threshold cascade, whose last arm returns whatever it
was given, so a negative passes through intact and reads as a figure the engine reported. With the key absent the tab draws its own “No storage size
information available.” instead, and the Tables / Indexes cards read N/A. Both arms are pinned
in tests/components/monitoring/StorageTab.test.tsx.
getHealth()’s size is a required string, so its absence is spelled "N/A" - the value that
method’s own catch already returns. That one does reach the model: the curated health reading
projects databaseSize verbatim, so "0 B" told the model a database it could not measure holds
nothing.
Three 0s stay in getOverview()’s catch, for two different reasons - and neither reason is that
something was measured:
maxConnectionsstays0because there0means “no limit published” - absence and zero are the same fact for a published ceiling, which is why the field is a required number (DatabaseOverviewin types.ts);tableCountandindexCountare required numbers with no absence to spell, so0is the only value the type leaves for a path that counted nothing. They are the one place this object still states more than it read (§13).
8. Maintenance
Section titled “8. Maintenance”runMaintenance(type, target?) (mongodb.ts)
maps the generic operations onto MongoDB admin commands:
| Type | MongoDB action |
|---|---|
analyze |
validate (one collection, or every collection) |
vacuum / optimize |
compact (one collection, or best-effort all) |
check |
dbCheck (requires a collection target) |
kill |
killOp (requires an opid) |
reindex |
unsupported — returns a message (the reIndex command was removed in MongoDB 6.0+) |
getCapabilities().maintenanceOperations = ['vacuum', 'analyze', 'check'] — so the UI surfaces those
three, though runMaintenance also accepts optimize/kill/reindex when invoked directly.
Where each operation may be offered (maintenanceOperationSpecs)
Section titled “Where each operation may be offered (maintenanceOperationSpecs)”Declaring that an operation EXISTS is not enough to put a button on it: two engines that
declare the same MaintenanceType take different kinds of target, so each provider also
declares what its own operations may be pointed at. The monitoring Tables tab renders a
per-row control only where perEntity is true, the admin Operations tab a whole-database
card only where global is true, and both take the wording from label (#496).
POST /api/db/maintenance reads the same declaration since #U20, and it is the one reader that
REFUSES rather than hides: it takes the placement from whether the request carries a target
(absent or empty means whole-database) and answers 400 when this provider marks that
placement unavailable while the other one is available - a targetless {type:"check"} is that
request here.
| Operation | Control label | Per-row | Global | Why |
|---|---|---|---|---|
vacuum |
Compact Collection | yes | yes | {compact: <coll>}, or every collection from listCollections() |
analyze |
Validate Collection | yes | yes | {validate: <coll>}, same loop without a target |
check |
Check Collection | yes | no | {dbCheck: <coll>} is not looped and throws without a collection name |
“Compact Collection” really is the vacuum this provider declares, so
vacuumActionOperation stays absent.
9. Capabilities & labels
Section titled “9. Capabilities & labels”getCapabilities() (mongodb.ts)
Section titled “getCapabilities() (mongodb.ts)”| Capability | Value |
|---|---|
queryLanguage |
json |
supportsExplain |
false |
supportsExternalQueryLimiting |
false |
supportsCreateTable |
false |
supportsInlineRowEdit |
false — the query language is JSON commands, so there is no UPDATE ... SET for the results grid’s inline editor to emit |
supportsTransactions |
false — multi-document transactions need a client session this provider does not hold, so BEGIN/COMMIT/ROLLBACK and SANDBOX are not offered; they used to be, and answered HTTP 400 (#464) |
declaresForeignKeys |
false — MongoDB has no foreign key constraint at all, so an empty foreignKeys list here is the engine’s model and not this database’s shape |
supportsMaintenance |
true |
maintenanceOperations |
['vacuum', 'analyze', 'check'] |
supportsConnectionString |
true |
defaultPort |
27017 |
schemaRefreshPattern |
"operation"\s*:\s*"(insert|delete|update) |
containerLevels |
one level, { id: 'schema', label: 'Database' } — the object surface’s container (§6) |
objectKinds |
collection (relation, acceptsRowWrites) and view (relation). No index, no routine kind, no timeseries kind; each absence is measured in §6 |
schemaRefreshPattern matches write operations in the JSON query so the UI refreshes collections
after inserts/updates/deletes.
Labels — overridden (getLabels(), mongodb.ts)
Section titled “Labels — overridden (getLabels(), mongodb.ts)”Document vocabulary: entity → Collection, row → document, select → Find Documents, analyze → Validate Collection, vacuum → Compact Collection, search → Search collections or fields….
statementLanguage is the one label a person never sees: the agent’s plan contract states it
verbatim to the model. It carries the JSON envelope of §3.1 and names
mongosh as the form that is excluded. It exists for the reason the search products’ does — told
to write “one runnable statement in this MongoDB database’s own query language”, a plan run on
2026-08-22 answered db.orders.aggregate([{ $group: … }]), which is correct MongoDB and unrunnable
here, because query() parses the JSON command object and nothing else. Naming only what the
language is did not survive contact with the model’s prior; naming what it is not did.
slowQueriesEmptyState (“Query stats come from the database profiler - run db.setProfilingLevel()
to start recording into system.profile.”) is the monitoring Queries panel’s empty state. That
sentence was hardcoded to PostgreSQL’s pg_stat_statements advice on every engine
(#463); here getSlowQueries() reads system.profile
(§7), which does not exist until the profiler is switched on.
10. Error handling
Section titled “10. Error handling”MongoDB uses the shared mapDatabaseError() (errors.ts) with no
MongoDB-specific branches:
| Situation | Error |
|---|---|
Missing host/database (no connection string) |
DatabaseConfigError |
Operation before connect() |
DatabaseConfigError (via ensureConnected()) |
connect() fails |
ConnectionError (carries host/port) |
Missing collection/operation, or invalid JSON |
QueryError (with a format example) |
Missing documents/update for a write op |
QueryError |
| Authentication failure (message contains authentication) | AuthenticationError |
| Other driver errors | generic QueryError / DatabaseError with the original message |
11. Testing
Section titled “11. Testing”Integration tests live in
tests/integration/db/mongodb-provider.test.ts,
mocking the mongodb driver via mock.module('mongodb', …) before the provider is imported. The
mock collection/cursor/admin returns canned documents and stats, exercising every operation, BSON
serialization, schema inference, monitoring, and maintenance.
⚠️ Mock isolation:
bun’smock.module()is process-wide; files mocking different drivers cross-contaminate in a shared process. CI runs the full suite viabun run test:ci(per-file process isolation viatests/run-core.sh) andbun run test:coveragefor determinism. Thebun run testpre-commit gate (perCLAUDE.md) also works — it isolates the component group — but runs the core group in a single process, so prefertest:ciwhen isolation matters. Running a single file alone is always safe.
Coverage
Section titled “Coverage”Validation, connect/disconnect, capabilities, labels, prepareQuery, every query operation
(find/aggregate/count/distinct/insert/update/delete), column inference, health, maintenance,
overview, performance, slow queries, active sessions, table/index/storage stats, BSON
serialization (ObjectId/Binary/Decimal128/Date/nested), getMonitoringData, and every ssl.mode
branch asserted against the options object the MongoClient constructor received.
The object-surface fixture
Section titled “The object-surface fixture”docker/mongodb-init/01-object-fixture.js is the
database the object-surface tests reason about, and it is a committed deliverable rather than
scaffolding: a live measurement nobody can re-run is not evidence. It is mounted in
database-compose.yml at /docker-entrypoint-initdb.d, which the
mongo image runs through mongosh once, on a fresh data directory only — an already
initialized container has to be recreated before an edit takes effect.
# Fresh container with the fixture applied through the mountdocker compose -f database-compose.yml up -d mongodbdocker logs libredb-mongodb 2>&1 | grep 'libredb object fixture applied'
# Or against a container that is already initialized, without recreating itdocker cp docker/mongodb-init/01-object-fixture.js <container>:/tmp/01-object-fixture.jsdocker exec <container> mongosh -u <user> -p <password> --quiet --file /tmp/01-object-fixture.js
# What it builds: databases `app`, `configstore` and `oddnames`# app.customers, app.orders, app.systemetrics collections# app.readings a TIME SERIES collection (type: "timeseries")# app.active_customers a view on app.customers# by_thing the SAME index name on two collections# configstore.settings a database whose name starts with "config"# oddnames.{x"a, x-a, x\a} names that sort differently under JSON escapingConnect the provider against the app database. Every object above exists to make one claim about
the engine re-measurable; §6 says which claim each one carries.
bun test tests/integration/db/mongodb-provider.test.ts # just this filebun run test:ci # CI publish gatebun run test:coverage # CI coverage workflowTo smoke-test against a live server: docker run --rm -p 27017:27017 mongo:7, then connect to
mongodb://localhost:27017/test in the dbportal UI.
12. Usage examples
Section titled “12. Usage examples”import { createDatabaseProvider } from '@/lib/db/factory';
const provider = await createDatabaseProvider({ id: 'mg1', name: 'App', type: 'mongodb', connectionString: 'mongodb://localhost:27017/app', createdAt: new Date(),});
await provider.connect();const res = await provider.query(JSON.stringify({ collection: 'users', operation: 'find', filter: { active: true }, options: { limit: 50 },}));const objects = await provider.listObjects(container, 'table');const { details } = await provider.describeObjects(container, 'table');await provider.disconnect();Over the API: POST /api/db/query (JSON MQL in the sql field) and POST /api/db/maintenance
(admin). Transaction/cancel routes do not apply.
13. Known limitations & future work
Section titled “13. Known limitations & future work”- Schema is inferred from a 100-document sample. Fields outside the sample don’t appear; subdocuments are expanded only to depth 3 and only up to 200 fields per collection, and array elements’ fields are never expanded (§3.3).
aggregateresults are unbounded. Onlyfindgets a default 100-document cap; anaggregatepipeline without$limitcan return a very large result set (§3.4). Future: inject a safety$limit/ cap aggregate output.- No
EXPLAIN. MongoDB’sexplain()is not wired (supportsExplain: false). - No multi-document transactions. MongoDB supports them on replica sets/sharded clusters, but the provider exposes no begin/commit/rollback API.
- No
cancelQuery. A running operation can only be terminated via maintenancekillOp(needs the opid and privileges). - No column modification in a generated migration. Since
#269 the schema-diff migration generator
answers a modified column per dialect; collections are schemaless, so it emits
-- MongoDB: Cannot alter column "<name>". ...where it previously emitted PostgreSQLALTER TABLE ... ALTER COLUMNDDL that means nothing here. collStatsis deprecated in MongoDB 6.2+ (in favour of the$collStatsaggregation stage); size/stats calls may warn or change on newer servers.- Monitoring needs privileges.
serverStatus/currentOp/$indexStatsand the profiler require appropriate roles (clusterMonitor, etc.); without them the affected metrics are reported as unavailable rather than as numbers (§7.1), the health connection count is omitted rather than reported as0(§7.2), the overview’s byte figure is omitted rather than reported as0(§7.3), and slow queries require the profiler to be enabled. - A failed overview read still reports
tableCount: 0andindexCount: 0. Both fields are required numbers onDatabaseOverview, sogetOverview()’s catch has no absence to spell for them and a deniedserverStatusreports two counts nobody took (§7.3). Future: the same change the connection count and the byte figure got, which needs the two fields to become optional across all 17 type-ids. Binaryvalues are shown as a placeholder (<Binary: N bytes>), not the raw bytes, and only a subset of BSON types are normalised (Long/Timestamp/UUID/RegExp/Code/DBRefrender as generic objects).findOnesilently ignoressort/skip/limit(onlyprojectionis honoured), so it cannot be used to fetch “the latest” document by sort.aggregateignoresoptions.limit/skipand has no safety cap — only an in-pipeline$limitbounds the result set (supportsExternalQueryLimiting: false, so the route injects none).- The active-sessions
usercolumn shows the client address (op.client, e.g.host:port), not an authenticated user. Future: map fromop.effectiveUsers/op.users(MongoDB 5.0+). getIndexStats().indexTypeonly distinguishestextvsbtree—hashed, geospatial (2dsphere/2d), wildcard ($**), and clustered indexes are all reported asbtree.- The
unlimitedquery option is ignored.prepareQuery()always returnslimit: options.limit || 100; combined with the route’shasMore = rows.length === prepared.limit, an “unlimited” request can report an incorrecthasMore. - A folder’s columns are SAMPLED, and the sample is bounded per collection.
describeObjectsreads a whole container-and-kind folder in one$unionWithchain rather than one call per collection, chunked atSAMPLE_CHUNK_SIZE = 100collections per pipeline so a wide folder cannot outgrow MongoDB’s own pipeline-length ceiling. So the round trips grow as the folder divided by 100, not with it. What the read still cannot tell you is a field no sampled document carried (§6).
14. References
Section titled “14. References”- Driver:
mongodb(node-mongodb-native) - MongoDB manual:
serverStatus·dbStats— the two commands §7.1–§7.3 read, and the pages the claims about which output fields are guaranteed come from - Source:
src/lib/db/providers/document/mongodb.ts - Base class:
src/lib/db/base-provider.ts - Interface & DTOs:
src/lib/db/types.ts - Errors:
src/lib/db/errors.ts - Tests:
tests/integration/db/mongodb-provider.test.ts - API contract:
docs/API_DOCS.md· query format also inCLAUDE.md - Sibling provider docs: PostgreSQL · MySQL · Oracle · SQL Server · SQLite · Apache Trino · Redis