Bringing MongoDB into a desktop client raises a problem relational engines don't have: there is no declared schema, no SQL, and the query language is a document API. QoreDB still has to fit this engine into the same universal model as PostgreSQL or SQLite without betraying how it actually works. This article details how the MongoDB driver does it, and what it deliberately refuses to do.
All the code described here lives in src-tauri/crates/qore-drivers/src/drivers/mongodb.rs and its companion validation module src-tauri/crates/qore-drivers/src/mongo_pipeline.rs. The driver builds on the official MongoDB client for Rust and implements the same DataEngine trait as every other engine.
A document engine projected onto Namespace, Collection, Row
QoreDB's data model rests on three levels: Namespace, Collection, and Row. The MongoDB mapping is direct: a database becomes a Namespace, a collection stays a Collection. The less obvious part is the row: a BSON document has no fixed columns.
The driver settles this by exposing each document as a Row with a single column named document of type json. The conversion happens in document_to_row, which serializes the BSON Document into a JSON value. Rather than artificially flattening a nested document into columns, QoreDB preserves its structure as-is.
A JSON query interface, not a Mongo shell
MongoDB has no textual language like SQL. The driver therefore expects a JSON-formatted query, parsed by parse_query: an object with the fields database, collection, an optional query, and an operation field. That last one is normalized (lowercased, underscores stripped) then dispatched. A db.collection shorthand is still accepted for ad-hoc reads.
The dispatch covers most operations: find, insertOne/insertMany, updateOne/updateMany, deleteOne/deleteMany, aggregate, count, distinct, along with bulkWrite and the findOneAndUpdate / findOneAndDelete / findOneAndReplace family. Index and collection operations (createIndex, dropIndex, createCollection, dropCollection) round out the list.
{
"operation": "find",
"database": "shop",
"collection": "orders",
"query": { "status": "paid" }
}BSON to JSON, and the ObjectId trap
Converting QoreDB's internal values to BSON goes through value_to_bson. Simple types map directly, but one choice is worth noting: a string that parses as a valid ObjectId (24 hexadecimal characters) is converted to Bson::ObjectId rather than Bson::String. Without this behavior, filtering on an identifier pasted from the UI would never return anything, because a ObjectId does not equal its textual representation in BSON.
Symmetrically, row_data_to_document skips a null or empty _id field on insert, which lets MongoDB generate the identifier itself. Pattern searches go through escape_regex, which escapes special characters so a typed term is treated literally rather than as a regular expression.
Aggregation on an allow-list, not a deny-list
The aggregation pipeline is the most sensitive spot: it is a full server-side processing language. Rather than banning a few dangerous operators, QoreDB does the opposite in validate_pipeline. Only the stages explicitly listed in the StageKind enum are accepted; any unknown operator is rejected by default. The number of stages is capped by MAX_PIPELINE_STAGES, set to 50, and the write stages $out and $merge may only appear at the end of the pipeline.
Three operators are banned regardless of position, because they run arbitrary JavaScript server-side: $function, $accumulator, and $where. The assert_no_forbidden_operators function searches for them recursively down to a depth of MAX_SCAN_DEPTH (64 levels), and the same check applies to a find filter, so that a $where cannot bypass the ban by going through a plain read.
/// Server-side JavaScript / arbitrary code execution operators.
/// Forbidden regardless of where they appear in a stage body.
const FORBIDDEN_OPERATORS: &[&str] = &["$function", "$accumulator", "$where"];A schema inferred by sampling
Because MongoDB is schemaless, describe_table cannot query a catalog. Instead it samples up to 100 documents and infers a type per observed field: int32, int64, string, ObjectId, datetime, array, document, and so on. The _id field is always pinned first, marked as the primary key, and the other columns are sorted alphabetically.
This is a synthetic view, not a contract. A field missing from the first 100 documents will not appear, and a field with a varying type is reported based on its first occurrence. QoreDB therefore shows what the collection actually exposes at the moment of inspection, without claiming a fixed schema that does not exist in the engine.
What stays out of reach
Transactions are the clearest example. detect_transaction_support runs the hello command (falling back to isMaster) to find out whether the instance is part of a replica set or sharded cluster and exposes logical sessions. On a standalone instance, begin_transaction returns an explicit not_supported error. That is an engine constraint, not a QoreDB one: MongoDB itself refuses transactions outside a replica set.
Cancellation follows the same honesty. cancel_support returns CancelSupport::BestEffort: the driver aborts the local async task via an AbortHandle, but cannot guarantee a server-side stop. Finally, maintenance operations are limited to compact and validate; anything else returns not_supported rather than a rough simulation.
A memory guard for the non-streaming path
The non-streaming execute path accumulates documents into a Vec before returning the result. A find or aggregate over a 100-million-document collection would therefore blow up the client's memory. The MAX_NON_STREAMING_ROWS constant, set to one million, acts as a last-resort fuse; the command layer can apply a stricter limit via SafetyPolicy.max_result_rows. For large volumes, the execute_stream path takes over, with bounded memory.
A consistent stance
The MongoDB driver captures QoreDB's line well: expose what the engine actually does, refuse arbitrary code execution, and don't invent a schema where there is none. BSON is preserved rather than flattened, aggregation is locked behind an allow-list, and features that depend on the deployment — transactions, hard cancellation — are flagged as such instead of being faked. An honest database client beats one that promises more than the engine delivers.
Stay updated on new releases
Subscribe to get product releases, new drivers notifications, and technical tutorials.

