Reference
Compatibility matrix
What happens when your code hits an edge of the emulation. The companion page, Supported operations, lists what is inside the boundary; this one is about the boundary itself: every named edge, with the matrix key its error carries.
The three outcomes
Every operation you can throw at the shim lands in exactly one of three places:
- Supported: the operation is emulated, and the differential lane in CI runs it against a real MongoDB server to check the answers match. The full list, family by family, is on Supported operations.
- Named fail-fast: the operation is deliberately not emulated. It raises
MongoTestUnsupportedExceptionnaming the feature and a stable matrix key. The tables below enumerate these keys. - Server-shaped error: the input is one a real
mongodwould reject too. The shim raises the same driver exception type with the server's error code and wording, because pointing you at this matrix for a query that would also fail in production would be misleading. See below.
There is deliberately no fourth outcome. An operation that ran but answered differently from real MongoDB is a bug in the shim, not a documented limitation. That invariant is what the differential lane enforces.
Anatomy of a fail-fast error
MongoTestUnsupportedException extends com.mongodb.MongoException, so it surfaces through repositories and templates the way any driver error does. The message always has the same shape:
com.mongodb.MongoException:
Unsupported by bonsai: aggregation stage $facet (see compatibility matrix: aggregation.facet)The feature and the key are also available programmatically: feature() and matrixKey(). A test can assert on the boundary rather than on message text:
MongoTestUnsupportedException boundary = catchThrowableOfType(
MongoTestUnsupportedException.class,
() -> repository.searchFacets(query));
assertThat(boundary.matrixKey()).isEqualTo("aggregation.facet");
assertThat(boundary.feature()).isEqualTo("aggregation stage $facet");Query operators
Filters run through the emulation's filter translator. The supported operator set, from $and through $elemMatch, is on Supported operations. At the edges:
| Matrix key | The edge it names |
|---|---|
query.$<operator> | Any refused or unrecognised operator under a field, by name: $where$expr$jsonSchema$text$slice$bitsAllSet$bitsAnySet$bitsAllClear$bitsAnyClear$near$nearSphere$geoWithin$geoIntersects$geoNear plus, at the top level, $comment$sampleRate$alwaysTrue$alwaysFalse. The key keeps the dollar sign: $text fails with query.$text. |
query.nested-elem-match | An $elemMatch nested directly inside another $elemMatch. |
query.collation-matching | A collation set together with a non-empty filter is refused so a collation never silently fails to affect $eq, $in or range comparisons. |
query.collation-sort | A find sort collation outside the accepted shape (a resolvable locale at primary or secondary strength). |
An unknown top-level operator, such as {$status: "NEW"} where a field name was meant, is the server-shaped case. Real MongoDB rejects it too, so the shim raises the server's BadValue error rather than a matrix key.
find: projection, sort, collation
| Matrix key | The edge it names |
|---|---|
projection.operator | Projection operators the emulation does not implement ($slice, $elemMatch and friends inside a projection). |
projection.exclude | Exclusion projections beyond the forms the shim can express. |
sort.specification | Sort specifications outside plain 1/-1 field directions. |
Writes and updates
The 14 supported update operators and their modifiers are on Supported operations. At the edges:
| Matrix key | The edge it names |
|---|---|
update.<operator> | Known MongoDB update operators that the shim does not implement fail fast by bare name: $bit fails with update.bit. A name MongoDB does not define is instead returned as its FailedToParse write error (code 9). |
update.pipeline | Aggregation-pipeline updates: every updateOne/updateMany/findOneAndUpdate overload taking a pipeline, and pipeline updates inside a bulkWrite model. |
update.currentDate-timestamp | $currentDate with {$type: "timestamp"}: the date type is supported. |
update.pull-condition | A $pull condition outside the comparison operators ($regex, $elemMatch and the rest). |
update.collation | hint | let | sort | Those options on update, replace and findOneAndUpdate/findOneAndReplace calls. |
delete.collation | hint | let | Those options on deletes and findOneAndDelete. |
count.collation | hint | maxTime | Those options on counts. |
insert.comment | The comment option on inserts. |
insert.unordered | ordered=false inserts: the emulation is ordered-only, and says so rather than silently reordering semantics. |
driver.bulkWrite.model | A bulkWrite model outside the six supported ones. |
Genuine write errors stay server-shaped. Ordinary insert, update and replace write commands report duplicate keys as MongoWriteException (E11000) and malformed updates through the write-error family. findOneAndUpdate and findOneAndReplace use the findAndModify command instead, so both malformed operands and duplicate upserts surface as MongoCommandException. the command family a real MongoDB server uses. updateOne remains in the write-error family.
Values and codecs
Values are encoded the way the driver encodes them: whatever type you put in a document, a filter or an update operand is looked up in that collection's effective codec registry, the one the client, database, or collection carries, including anything you added with withCodecRegistry(...), and stored as whatever its codec writes. That is the same lookup a real driver performs on the wire, so the stored value is the same one a real server would hold.
Concretely: BigDecimal goes through the driver's own BigDecimalCodec and is stored as a Decimal128. A Codec you wrote yourself decides its own type's representation. Reading a raw Document back gives you what the wire holds: a Decimal128, not a BigDecimal, exactly as a real find does; a typed read through a POJO codec restores the Java type, again exactly as the driver does. No type is on a whitelist, and none needs to be.
| Matrix key | The edge it names |
|---|---|
codec.unsupported-type | A value no codec in the effective registry can encode is the case where the real driver raises CodecConfigurationException client-side before anything is sent. It also covers the handful of types that have a codec but encode to a BSON type the embedded store cannot hold: MinKey, MaxKey and BSON undefined. If you hit this for a type of your own, register a Codec for it. In a MongoClientSettingsBuilderCustomizer bean, or with withCodecRegistry(...), it will store. |
codec.reserved-field | A field literally named __mid, which is where the store keeps your _id. |
codec.dotted-field-names | A field name containing a dot, at any nesting level. |
codec.uuid-unspecified | A raw java.util.UUID under the driver's default uuidRepresentation (UNSPECIFIED). The real driver refuses to encode it client-side (“The uuidRepresentation has not been specified”); the shim currently stores it as binary subtype 4. The shim is the more permissive of the two here, pinned by the differential lane as a known divergence. Set a uuidRepresentation on your client, as production code must anyway, and both sides encode: with STANDARD they agree exactly (binary subtype 4). The legacy representations (JAVA_LEGACY, C_SHARP_LEGACY, PYTHON_LEGACY) write binary subtype 3 with their own byte order, which the shim does not reproduce. The stored bytes therefore differ even though the round-trip through your own client does not. |
Aggregation
The supported stages, expressions and accumulators are on Supported operations. At the edges:
| Matrix key | The edge it names |
|---|---|
aggregation.<stage> | Any unrecognised stage, by bare name: $facet fails with aggregation.facet; likewise $out, $merge, $bucket, $graphLookup, $setWindowFields, $search and the rest. |
aggregation.lookup | The pipeline/let form of $lookup: the equality form is supported. |
aggregation.expression.<op> | Any unrecognised expression operator: $regexMatch fails with aggregation.expression.regexMatch. A {$meta: ...} sort key lands here too, as aggregation.expression.meta. |
aggregation.expression.variable | Any variable other than $$ROOT and $$CURRENT. |
aggregation.expression.timezone | The timezone option on the date expressions is refused rather than silently evaluated at UTC. |
aggregation.group.<op> | Any unrecognised accumulator: $stdDevPop fails with aggregation.group.stdDevPop. |
aggregation.collation | The collation option on aggregate calls. |
Indexes
Single-field and compound indexes with unique and custom names are supported. See Supported operations. Every other index feature fails fast:
| Matrix key | The edge it names |
|---|---|
index.ttl | expireAfterSeconds: a TTL that never fires would make expiry tests pass vacuously. |
index.sparse | partial | hidden | sparse, partialFilterExpression, hidden. |
index.wildcard | text | hashed | geo | Wildcard ($**, wildcardProjection), text (including weights), hashed, and the geospatial key types. |
index.collation | storageEngine | Index-level collation and storageEngine options. |
index.keyType | Any other unrecognised key type or key value. |
index.search | Atlas search indexes: the whole API family. |
index.mixedTypes | A runtime edge, not an option: two BSON types in one indexed field. The underlying store orders index entries by the values' own ordering and cannot hold both, so the shim names the situation instead of corrupting the read path. |
Driver surface
Driver methods the shim does not implement all fail fast as driver.<method>: driver.watch (change streams), driver.mapReduce, driver.renameCollection, index dropping, runCommand, createView, database-level aggregation, and the rest of the unimplemented surface. A few boundaries carry a named row of their own instead, because one bare method name would describe them wrongly. See driver.clientBulkWrite and driver.rawDocumentClass below. Concern and preference views are part of the supported surface:
- Supported and carried: client, database and collection
withWriteConcern,withReadConcernandwithReadPreferencereturn immutable views carrying the requested value; their matching getters return it. New views start with the driver defaults:ACKNOWLEDGED,DEFAULTandprimary(). Majority, W1, journaled and timed write concerns, every read-concern level, and every read preference are accepted. In the synchronous single-node store they do not change the result: an acknowledged write is already durable and a read sees committed state. - Accepted no-ops:
allowDiskUseandbatchSizeon aggregates are accepted and ignored, because they are performance hints with no observable semantics here.
| Matrix key | The edge it names |
|---|---|
driver.clientBulkWrite | The client-level MongoClient.bulkWrite(...) family (driver 5.x), all four overloads: one command writing across several namespaces at once. The collection-level MongoCollection.bulkWrite is emulated and unaffected, which is why this row is named rather than left to driver.bulkWrite: a key naming the bare method would read as a refusal of something that works. |
driver.rawDocumentClass | RawBsonDocument as a collection's document class, through withDocumentClass(...) or getCollection(name, RawBsonDocument.class). That class is a promise about bytes, the exact BSON the server sent, and the differential lane that certifies fidelity compares canonical Extended JSON, a value comparison that cannot establish equality of the backing BSON bytes. Byte parity is therefore a claim nothing here can prove, so it is refused at declaration time rather than served by re-encoding. Support can only arrive together with a byte-level comparer. BsonDocument, Document, POJOs and records are unaffected. |
changeStream.* | The unsupported edges of the experimental change-stream surface, each by name: changeStream.client.watch (the deployment-wide client.watch(), the collection and database forms are supported), changeStream.startAtOperationTime, changeStream.showExpandedEvents, changeStream.collation, changeStream.pipeline.stage (any stage beyond $match/$project/$addFields), and changeStream.match.operator (a $match operator other than equality and $in). The whole family only applies with bonsai.experimental.change-streams=true; with the flag off, watch() itself is driver.watch, exactly as in 3.4.0. See Change streams. |
driver.writeConcernMultiNode | A numeric write concern of w >= 2. The single-node store can never receive the requested acknowledgement from a second member; a real single-node replica set waits indefinitely, so the shim refuses client settings, transaction options, and views carrying it. |
driver.writeConcernUnacknowledged | w:0 (unacknowledged, fire-and-forget). It swallows write errors, including duplicate-key failures; a test harness that hid those failures would defeat its purpose, so the shim refuses client settings, transaction options, and views carrying it. |
driver.writeConcernTag | A string w other than majority names a replica-set tag write concern. Tag modes need a matching lastErrorModes replica-set configuration, which the embedded store does not have; a real single-node replica set without it rejects the write with UnknownReplWriteConcern. |
Sessions and transactions
The session and transaction API is substantially implemented. See Supported operations and the Transactions page for the isolation contract. The collection session overloads for CRUD, find-and-modify, distinct, aggregation (including $lookup) and bulkWrite all use the transaction overlay for reads and, where applicable, staged writes. The named edges:
| Matrix key | The edge it names |
|---|---|
session.setTransactionContext | The one session method that fails fast by name, rather than silently pretending to pin a server would be misleading. The other context accessors are inert. |
session.foreign | A ClientSession the embedded store did not create, a foreign session would silently do nothing, so it is refused. |
transaction.uniqueIndexTiming | A secondary unique-index violation inside a transaction raises the server's MongoWriteException with the same error code (E11000), but at commit, where a real replica set fails at the offending write. The differential lane pins the class, category and code of this one path; it does not compare error messages, so do not assert on the text. |
crud.replaceNaturalOrder | replaceOne/findOneAndReplace re-store the document, so an unindexed find() returns it at the end of natural scan order, where a real server (WiredTiger, keyed by RecordId) leaves it in place. Counts, matches and content are identical; only the natural order of an unsorted read differs. If a test depends on document order, sort explicitly, as production code must anyway, since the server documents natural order as unspecified. |
Snapshot sessions are refused with the driver's own wording (“Transactions are not supported in snapshot sessions”) at startTransaction().
When you get a server error instead
A matrix key means the library declined. When the input is at fault, meaning something a real server rejects, the shim raises the same driver exception with the server's error code and code name, because that is the error your code must handle in production too:
// An unknown TOP-LEVEL operator is a server error, not a library boundary:
// this filter fails against real MongoDB too, with the server's own wording:
com.mongodb.MongoCommandException (code 2, BadValue):
"unknown top level operator: $status. If you have a field name that starts
with a '$' symbol, consider using $getField or $setField."Other examples: $project and $unwind specifications of the wrong type raise the server's location codes (15969, 15981), an empty index key document raises CannotCreateIndex (67), and the {db, coll} namespace form of $lookup's from raises FailedToParse (9). Each is pinned against real server behaviour by the differential lane.