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 MongoTestUnsupportedException naming the feature and a stable matrix key. The tables below enumerate these keys.
  • Server-shaped error: the input is one a real mongod would 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:

text
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:

Java
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 keyThe 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-matchAn $elemMatch nested directly inside another $elemMatch.
query.collation-matchingA 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-sortA 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 keyThe edge it names
projection.operatorProjection operators the emulation does not implement ($slice, $elemMatch and friends inside a projection).
projection.excludeExclusion projections beyond the forms the shim can express.
sort.specificationSort 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 keyThe 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.pipelineAggregation-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-conditionA $pull condition outside the comparison operators ($regex, $elemMatch and the rest).
update.collation | hint | let | sortThose options on update, replace and findOneAndUpdate/findOneAndReplace calls.
delete.collation | hint | letThose options on deletes and findOneAndDelete.
count.collation | hint | maxTimeThose options on counts.
insert.commentThe comment option on inserts.
insert.unorderedordered=false inserts: the emulation is ordered-only, and says so rather than silently reordering semantics.
driver.bulkWrite.modelA 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 keyThe edge it names
codec.unsupported-typeA 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-fieldA field literally named __mid, which is where the store keeps your _id.
codec.dotted-field-namesA field name containing a dot, at any nesting level.
codec.uuid-unspecifiedA 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 keyThe 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.lookupThe 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.variableAny variable other than $$ROOT and $$CURRENT.
aggregation.expression.timezoneThe 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.collationThe 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 keyThe edge it names
index.ttlexpireAfterSeconds: a TTL that never fires would make expiry tests pass vacuously.
index.sparse | partial | hiddensparse, partialFilterExpression, hidden.
index.wildcard | text | hashed | geoWildcard ($**, wildcardProjection), text (including weights), hashed, and the geospatial key types.
index.collation | storageEngineIndex-level collation and storageEngine options.
index.keyTypeAny other unrecognised key type or key value.
index.searchAtlas search indexes: the whole API family.
index.mixedTypesA 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, withReadConcern and withReadPreference return immutable views carrying the requested value; their matching getters return it. New views start with the driver defaults: ACKNOWLEDGED, DEFAULT and primary(). 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: allowDiskUse and batchSize on aggregates are accepted and ignored, because they are performance hints with no observable semantics here.
Matrix keyThe edge it names
driver.clientBulkWriteThe 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.rawDocumentClassRawBsonDocument 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.writeConcernMultiNodeA 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.writeConcernUnacknowledgedw: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.writeConcernTagA 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 keyThe edge it names
session.setTransactionContextThe 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.foreignA ClientSession the embedded store did not create, a foreign session would silently do nothing, so it is refused.
transaction.uniqueIndexTimingA 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.replaceNaturalOrderreplaceOne/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:

text
// 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.