Skip to content

Plan mode on MongoDB, where the safety checks decline

The read-only guard and the identifier reader are SQL readers, so on a MongoDB draft they record that they do not apply instead of passing it.

A check that cannot read the language it is handed has two options. It can return a green tick, or it can say it did not look. On a MongoDB connection, LibreDB Studio’s plan mode takes the second one.

Agent AUTO mode — the tool-using, metered run that executes statements itself — does not run on MongoDB. The read-only execution profile it needs is database-native, and a provider-native queryReadOnly exists only on PostgreSQL, SQLite and DuckDB, so an auto run on a MongoDB connection ends engine-unsupported. Plan mode opens on every connection, including this one. It is toolless, it executes nothing, and it drafts one statement for a person to run.

That much is the same on every engine. What is different here is the two checks that run after the draft is written: the read-only guard and the identifier reader are both SQL readers, so on a MongoDB draft both record that they do not apply rather than passing it.

What plan mode reads before its first turn

Before the first turn the server grounds the run, and on a document engine that grounding goes through db.schema.read — an operation descriptor whose entire input is empty. It acquires agent-operations, calls this connection’s own provider.getSchema(), and returns structure. It is the inspection the sidebar performs when it lists your collections, taken through runAuditedAgentCall, so it meets the same mode check, deadline, budget clamp, audit record and artifact as any other agent read. There is no second, unaudited path to the database.

What comes back is inferred, not read from a catalog. getSchema() lists collections, skipping system.* and capping at 200, and for each one samples the first 100 documents to derive field types. Subdocuments expand to dotted paths to a depth of three, so shipping, shipping.city and shipping.geo.lat are all named and shipping.geo.deep.tooFar is not. Arrays are named and left closed, because items.sku addresses one value per array entry and does not mean there what it means on a subdocument. The field list caps at 200 fields per collection.

So the inventory the model is grounded on is a description of a sample. The preface says as much: the inventory is what the inspection found, not proof that nothing else exists. A plan run on 2026-08-22 grouped by $shipping.region, a path that database did not have, and MongoDB answers a missing path with one null group rather than an error — the draft read as runnable and was quietly wrong. That is why the depth-3 expansion names the container at each boundary instead of stopping at shipping: object.

Why the draft is a JSON command and not shell syntax

The provider extends BaseDatabaseProvider directly rather than the SQL base, and its query language is json. A statement here is a command object with a required collection and a required string operation, drawn from find, findOne, aggregate, count, distinct, insertOne, insertMany, updateOne, updateMany, deleteOne and deleteMany:

{
  "collection": "orders",
  "operation": "aggregate",
  "pipeline": [{ "$group": { "_id": "$status", "count": { "$sum": 1 } } }]
}

The plan contract states that envelope to the model verbatim, and states the negative with it: this is not mongosh shell syntax, and a statement that starts with db. cannot be run here. The rule is there because a live plan run on 2026-08-22 drafted db.orders.aggregate([...]) — correct MongoDB, and not something the editor can execute. Handing a user a statement the product cannot run is a worse failure than refusing to draft one, because it looks like success.

The two post-checks, and why both decline on this engine

Two checks run on a drafted statement before it is offered anywhere. Neither is more than it says it is.

Identifiers. Every table name the statement reads is compared against the captured inventory and unknown names are recorded on the event. It is a reader, not a parser: it prefers a miss to a false alarm, so an empty unknown list means “nothing recognised was missing”, never “the statement is sound”. It checks no columns. With no inventory it records no-inventory, because an empty list would be a claim that every table exists.

Read-only classification, through the same statement guard the rest of the agent surface uses. A statement that is not read-only is not blocked — that was ruled on deliberately — but it is marked on the event and visibly in the rail, so a hand-off never quietly gives someone a delete.

Both are SQL readers. Point them at a correct MongoDB draft and they are wrong at once, in opposite directions. The guard reads every string as SQL: db.orders.aggregate([...]) leads with the word DB, which is not in its read allowlist, so every correct MongoDB draft came back NON_READ_STATEMENT — structurally, since no command of this engine can lead with SELECT, VALUES, TABLE or EXPLAIN. The identifier half fails the other way: its table reader finds no table keyword, answers with an empty list, and the affirmative branch then reports that every table the statement names is in the inventory. That is a vacuous claim about names nothing looked at, and it is the more dangerous of the two, because it reads as a pass.

So validatePlanStatement takes the language into account and writes down what actually happened. guardApplicable: false, with no violation recorded, because there was no objection. readOnly: false, because a guard that read nothing has vouched for nothing. And identifiers: { kind: "not-applicable" } — its own variant, distinct from no-inventory, because an inventory usually was read on this path and what is missing is a reader, not a reading.

Reading a rail that blames the check rather than the draft

Three surfaces render that state: the timeline headline and its detail, the statement card, and the accessible name of the control that applies a draft to the editor. All three read the not-applicable state first and word it against the check’s reach, not against the statement. “We did not verify this” and “this failed verification” call for different actions from the person reading, and a rail that collapses them trains people to ignore it.

One more sentence belongs on the same card, on every engine: a statement validated against the inventory is still not a statement that will run. The inventory records what exists in this database, not what your role is permitted to read.

What is left for the person who runs it

What you still get: collection and field names captured from this connection at the start of the run rather than from the model’s memory of MongoDB in general; a statement in the envelope the editor actually parses; an audited record of the read that grounded it; and a run that ran no statement of yours and wrote nothing.

What you do not get, and must supply yourself:

Check On PostgreSQL or SQLite On MongoDB
Read-only classification Guard runs; writes are marked Declines: guardApplicable: false, readOnly: false
Identifier check Names compared to inventory not-applicable; no reader for this language
Execution Auto mode may run it under a read-only profile Auto mode ends engine-unsupported

Read the operation key first, because that is where the destructiveness lives. The execution confirmation gate reads the same JSON shape and treats deleteOne, deleteMany, updateOne, updateMany and top-level $out or $merge aggregate stages as destructive, so those ask before they run, exactly as a DELETE FROM does on a SQL engine. A payload it cannot read as a document with a string operation asks as well, rather than staying silent. That gate is a runtime control on your own execution, not a verdict on the draft; the published boundaries say what it does not cover.

Then check the result size, because the provider’s defaults are asymmetric. A find with no explicit options.limit is capped at 100 documents. An aggregate passes none of options to the cursor and has no default cap, so a pipeline with no $limit stage can return an unbounded result set. A drafted pipeline is exactly the shape where that bites.

A check that reports its own reach is one you can plan around. A check that returns a tick it did not earn is one you find out about later, holding a result you believed twice.