Repository navigation
v5.1.0
mongo4s 5.1.0
Mostly what services moving onto the direct codec path were writing by hand: a ScalarWireCodec that answers as the
BsonEncoder of its type, a repository over a WireCodec keyed on the entity's own fields, stored names, named scalar
representations and Map keys beyond String — plus cheaper decoding and rendering, and a fake that answers more like
the server. Binary compatible with 5.0.0, checked by MiMa with no exclusions. A few things change without a compile
error; upgrade notes:
COMPATIBILITY.md.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.1.0"New features
- A
ScalarWireCodecis theBsonEncoderandBsonDecoderof its type. Filters, updates, keys,selectAsand
distinctfind it in the type's companion or an import, sogiven BsonEncoder[X] = codec.toBsonEncoderis no
longer needed; one written by hand still wins.toBsonCodecon everyWireCodec, document-shaped ones included. BaseMongoRepository.directbuilds a repository overgetDirectCollectionand answers it as
F[Repository[F, S, E, K]], so the type it is assigned to fixesEandK— no type arguments, no.widen— and
the codec's naming applies without aFieldNamingargument:type PlayerRepository[F[*]] = Repository[F, CatsStream[F], Player, PlayerId] def players[F[*]: Async](db: MongoDatabase[F, CatsStream[F]]): F[PlayerRepository[F]] = BaseMongoRepository.direct(db, "players", projection = Projection.excludeId[Player])
objectIdDirectis the directobjectId:WithId[Id, E]has aWireCodecwheneverEis a derived case class.PrimaryKey.of(_.provider, _.providerId)keys an entity on one, two or three of its own fields as a plain tuple,
each field spelled through the collection's naming wherever the key renders,ensureKeyIndexincluded. A
PrimaryKeyforWithId[Id, E]over any id with aBsonEncoder, beside the one forObjectId.@storedand@notStoredon a field of a derived case class: the first stores it under the name given,
verbatim, the second keeps it out of the document and reads it back as its default orNone.Field.of,
PrimaryKey.ofandselectAsspell a@storedfield the same way and refuse a@notStoredone.- Sums shaped for collections someone else defined —
WireCodecConfig.withDiscriminatorFieldnames the field
that holds the case,withWrappedCaseswrites{"Circle": {"radius": 2.0}}, andwithFallbackCasereads a case
it does not know as acase object, for rolling deployments. - Named scalar representations —
ScalarWireCodec.instantAsEpochMillis,instantAsEpochSeconds,
instantAsIsoString,uuidAsBinary,localDateAsStringandlocalDateAsDate(zone), each ready to be made the
given where a collection already stores the type that way. AUUIDstored as binary subtype 4 reads on both paths.
ScalarWireCodec.byValue(values)(_.value)for an enum stored by a value of its own,WireCodec.decodeOnlyfor a
type that is only read. Mapkeyed by more thanString— aKeyCodec[K]writes each key as a field name:Int,LongandUUID
have one, and so does any type whoseScalarWireCodecwrites a string.field.at(key)reaches an entry in a query.Float,Short,Byte,CharandArray[Byte]on both codec paths, stored as the Java driver stores them,
with range checks on the way in.- Typed paths through an
Optionand into a collection's elements —itemsField / skuFieldisitems.sku, every
segment spelled through the naming.Field.of[E](_.field)names only the entity. - Joins that take the foreign collection —
Stage.lookup,lookupWith,lookupWithKeysandgraphLookupaccept
the foreignMongoCollectionand render its side through that collection's ownFieldNaming. - Repository methods services were writing by hand —
findOneByFilter,estimatedCount,updateByKeys,
updateEach,upsertKeeping,findOneAndReplace(entity)andfindOneAndDelete(key), each with a body on
Repository, so a hand-written implementation keeps compiling. - Operations —
MongoClient.ping(timeout);MongoClientResource.reachability(client, interval)inmongo4s-cats,
a background ping a readiness check reads;withTimeout(duration)on aMongoDatabaseor aMongoCollection;
MongoError.writeConcernErrorsandMongoError.WriteConcernFailure. - Smaller additions —
field.setNull;$textwithTextSearchOptions(language, case and diacritic
sensitivity);FakeRepository.directandFakeMongoCollection.direct;FakeRepositoryisopen, so a service's
own repository trait can mix into it.
Behaviour changes
field.set(None)renders$unset, not$set: null, so a document updated toNonehas the shape one inserted
with it has.setOnInsert(None)adds nothing;setNullwrites thenullwhere it is wanted.- A filter that matches nothing renders
{_id: {$in: []}}, answered from the_idindex, rather than
{$nor: [{}]}, which the server answered with a collection scan —in(Nil),containsAll(Nil),Filter.none, an
emptyKeyRef.inFilter,Filter.Or(Nil)and!Filter.all. - Refusals are raised in
F, not thrown where the call is built, soattemptandhandleErrorWithsee them. - On
zio, a defect stays a defect.attemptandhandleErrorWithrecover failures only, and a throw inside
map,flatMaporsuspendfails the effect, as on the other three runtimes. MongoErrorfiles failures where operations teams look for them — a clienttimeoutMSthat ran out is
ExecutionTimeout, refused credentials areUnauthorized, a stepped-down primary, a recovering node, a cleared
pool and a closed server areUnavailable.- Decoding failures name where they happened —
address -> city -> Expected string but got int— through
BsonError.Nested; a match on a bareTypeMismatchfinds it inside. Accumulator.sumandavgcompile only on a numeric field or anOptionof one. Code compiled against5.0
keeps linking.Filterhas a new case,TextSearch, which an exhaustive match has to handle.
Performance
- Decoding a derived entity — about 1.09× and 592 bytes less per document on a consumer-shaped entity: a
product no longer copies its fields into a tuple on the way to its constructor. - Rendering a query — 1.5× on a five-condition filter and 2.4× on a ten-operator update, with half the
garbage on the update. - An
$inover a thousand opaque ids renders 2.1× as fast and allocates what one over plain strings does.
Bug fixes
- On
kyo, an interrupt could leave a session open. One landing just aswithSessionor
client.withTransactionhad started its session skipped the release.bracketCaseinstalls its guard before it
acquires, and every finalizer runs inside kyo's scope. findManyreturned a document once per batch its key appeared in. The keys are made distinct before they are
batched.where(Field.self)rendered$[], updating every element instead of the ones an array filter selects.imaporiemapon a derived case class moved its fields undervaluewhen that class was a case of a sealed
trait or a branch of anEither. Documents already written in that shape still read through
WireCodec.from(codec, codec).- A sealed trait or an enum with more than 32 cases did not derive without raising
-Xmax-inlines. - A field whose codec reads
nullas a value was reported missing, and a_typethat is not a string failed with
the reader's own exception rather than aTypeMismatch. withTransactionre-committed a commit that ran out oftimeoutMS.- A bulk write that failed only on its write concern raised a
BulkWriteFailedwith nothing in it. - A date's
plusandminustook only aLongamount. - The fake failed on
null, mixed types and arrays in a range filter, a sort or$min/$max, throwing
cannot comparewhere the server answers; it compares in BSON's type order now. It also answers like the server on
numeric path segments, operators through arrays,$allon a scalar, a regular expression in$in, UTF-8 string
order, updates through array positions with the server's codes 28 and 14, a negative$position,$pullby a
document, number widths indistinctand$group, projections through arrays, and themodifiedCountof an
identical replacement — each pinned by a parity spec against MongoDB 8.2. - Docs — the website warned off
getDirectCollectionfor mixed numeric widths, a limit3.0.0removed; the
README now says that transactions do not nest, and when anEitherwith indistinguishable branches is refused.
Benchmarks
Re-measured on 5.1.0, every harness, against mongo4cats 0.8.0, with the machine, server and every library
version stated — BENCHMARKS.md.
WireCodecto real BSON bytes: 2.4–3.5× faster than eithermongo4catscodec, up to 6.8× less allocation.- Single-document operations against a real MongoDB land level for all six stacks. On bulk reads
mongo4cats
allocates 2.1–6.1× more onfindAll/findStreamand readsfindAll1.12–1.36× slower; its
find(...).streamruns at 1,471 ops/s againstmongo4s's 3,109 on the same effect system. - A new harness,
EffectBenchmark, measures a step of each runtime'sEffectinstance on its own: aflatMapis
12–31 ns everywhere, abracket190–246 ns oncats,zioandrapidand 2.5 µs onkyo— paid once per
session or transaction, not per document.
Compatibility
- Binary compatible with
5.0.0. New members ofRepository,MongoCollectionandMongoDatabasecarry bodies, so
an implementation of your own keeps compiling. Scala 3.9 LTS, with the integration tests compiled on3.10; class files target Java 17,mongo4s-kyoneeds
JDK 25 at runtime for kyo's sake, and building requires JDK 25. Tested against MongoDB 8.2.mongodb-driver-reactivestreams5.13.0,zio2.1.27,kyo1.0.0-RC7.mongo4s-kyoremains outside the
binary compatibility promise while kyo is on a release candidate.