Repository navigation
Releases
v6.0.0
mongo4s 6.0.0
mongo4s moves to the now.sendline organization. The code and API are the same as in 5.1.1, and the package is
still mongo4s. MiMa checks every module against org.mongo4s 5.0.0 and 5.1.1 and finds nothing. The upgrade is
one line in build.sbt.
libraryDependencies += "now.sendline" %% "mongo4s-core" % "6.0.0"Moving from 5.1.x
// before
"org.mongo4s" %% "mongo4s-cats" % "5.1.1"
// after
"now.sendline" %% "mongo4s-cats" % "6.0.0"A build tool treats a new organization as a new library and will not evict one in favour of the other. If something
in your graph still brings in org.mongo4s 5.x, both jars carry the same classes, and which one loads depends on
classpath order. Move that dependency as well, or exclude the old organization:
libraryDependencies += ("com.example" %% "internal-commons" % "1.2.3").excludeAll(ExclusionRule("org.mongo4s"))org.mongo4s 6.0.0 is published as a Maven relocation: one pom per module that points to now.sendline, with no
jar. A build that only bumps the version to 6.0.0 therefore resolves to the new organization, in Maven and in sbt
through Coursier. Maven also prints the relocation message saying where mongo4s moved. Nothing more will be
published under org.mongo4s.
Where things are now
- Repository: Sendline-now/mongo4s. The old URLs redirect.
- Site: oss.sendline.now/mongo4s, shared with logging4s and metrics4s and
built from this README.mongo4s.orgredirects there. - A new logo, in light and dark versions.
Why a major version
Nothing in the API changed. The version is major because switching the organization means editing build.sbt by
hand, and a version bump that looks routine would hide that. The reasoning is in
DESIGN.md, and the migration guide is in
COMPATIBILITY.md.
v5.1.1
mongo4s 5.1.1
Two fixes: a hang on large replies, which every runtime hits, and how kyo reports a cancellation. Binary compatible
with 5.0.0 and 5.1.0, checked by MiMa with no exclusions.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.1.1"Bug fixes
- A large reply read in many small pieces hung the call. MongoDB driver
5.6.0to5.13.0nests one callback
per partial socket read of a reply and unwinds them all at once when the reply is complete. About 900 reads
overflow the stack of the driver's I/O thread. TheStackOverflowErrornever reaches the caller, so thefind
waits forever (JAVA-6313). A link that delivers a few kilobytes per
read is enough to trigger it: TLS does, and so does Docker Desktop'shost.docker.internal, where a 3.5 MB reply
took over 1,500 reads. mongo4s now depends on driver5.14.0, which reads in a loop instead. - On kyo,
guaranteeCaseandbracketCasereported a cancellation as an error. kyo1.0.0-RC7abandons a fiber
interrupted while it waits and closes its scope withKyoException("fiber abandoned"). The finalizer therefore saw
ExitCase.Erroredinstead ofExitCase.Canceled. Transactions still rolled back, but code that tells the two cases
apart counted every timeout as a failure. The finalizer now seesCanceled.
Dependencies
mongodb-driver-reactivestreams,mongodb-driver-coreandbson5.14.0. A build that pins its ownorg.mongodb
jars needs5.14.0or later, or it still hangs.
Build
LargeReplyItSpecreads a 3 MB batch through a proxy that passes the server's answer on a kilobyte at a time. On
5.13.0it times out with the overflow in the log; on5.14.0it passes.cats,zioandkyoeach have a spec that interrupts a body insideguaranteeCase; none did before.
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, n...
v5.0.1
mongo4s 5.0.1
A fix for users on Scala 3.10. Binary compatible with 5.0.0, checked by MiMa with no exclusions.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.0.1"Bug fixes
- Code inlined into the caller carried Scala 3.10's deprecated
ArrowAssoc. Aderives WireCodecon a case
class with a default parameter, andPrimaryKey.compound, warned at the user's call site on 3.10, which a build
with-Werrorrefused to compile. Both build their pairs as tuples now.
Build
- The integration tests compile on Scala
3.10against the library built by3.9 LTS, so what the library inlines
into its callers is checked on the compiler they are moving to. With the5.0.0code they fail exactly as a user's
build did.
v5.0.0
mongo4s 5.0.0
Typed aggregation expressions, Atlas Search, the client-level bulkWrite, typed positional updates, conditional
writes and counters on the repository, and a fake that simulates upserts. A major release: several methods gained
parameters and several AST cases changed shape, so anything compiled against 4.x has to be recompiled, though most
calls compile unchanged. Migration notes:
COMPATIBILITY.md.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.0.0"New features
- Typed aggregation expressions —
Expr[E, A]computes a value from a document:field.expr,Expr.lit,
arithmetic, comparisons, logic,cond/switch, text, array (filter/mapover a typed element), date and
conversion operators, each compiling only where the server accepts it. They go into$addFields
("total" := price.expr * qty.expr),$groupkeys and accumulators,$replaceWith,$redact, computed
projections infindand$project,Filter.exprand pipeline updates. - Atlas Search —
$searchand$searchMetaover typedSearchOperators (text,phrase,autocomplete,
wildcard,regex,equalTo, ranges,exists,compound),$vectorSearchapproximate or exact with aFilter
pre-filter, search-index management with typed vector indexes, andExpr.searchScore. Tested against a
mongodb-atlas-localcontainer. - Client-level
bulkWrite(MongoDB 8.0+) —collection.write(command)addresses aWriteCommandwith that
collection's own codec, andclient.bulkWritesends writes to several collections and databases in one command. - Typed positional updates —
field.each($[]),field.matched($) andfield.where(element)($[id]),
withField.elementbuilding both the path and its array filter. pullWhereremoves array elements by a condition;Field.selfstates a condition on a scalar element.- Repository writes —
insertIfAbsent(entity): F[Boolean]inserts only when the key is free, without
overwriting and without failing a transaction;replace(entity, condition)replaces by key only while the
condition holds, for optimistic concurrency. - Counters —
MongoCounters.create(database, name)givesnext,currentandresetover one document per
counter,nextbeing a single atomicfindOneAndUpdate;FakeCountersin the testkit. WireCodecConfig.withDecodeMissingAsDefault— fill a missing field from the case-class default, for documents
written before the field existed. Off by default.- Options —
InsertOneOptions/InsertManyOptions(InsertManyOptions.unorderedinserts past a duplicate),
BulkWriteOptions,letvariables on every write and onfind/aggregate,sortonupdateOne/replaceOne
(8.0+), tailable cursors streamed live,maxAwaitTime,noCursorTimeout,allowDiskUse,hint/commenton
distinct,EstimatedCountOptions. - Smaller additions —
$typeover several types (hasAnyType),$currentDateas a timestamp,$unwind's
includeArrayIndex,$unionWithwith a pipeline, text-index weights and languages. - The fake simulates update-based upserts, building the inserted document from the filter the way the server
does, plus unordered inserts, asorton single-document updates,pullWhere, conditions on scalar elements,
$typeover several types and a timestamp$currentDate— each pinned by a parity spec against MongoDB 8.2.
Breaking changes
insertOne/insertManytake options, andbulkWritetakesBulkWriteOptionsinstead ofordered.Stage.addFieldstakes computed fields —"name" := expr— instead of(String, BsonValue)pairs.Stage.Group,Stage.Redact,Filter.Expr,Filter.Type,Update.CurrentDate,Projection.Include,
Stage.Unwind,Stage.UnionWithandWriteCommand.DeleteOne/DeleteManychanged shape.FindQuery,AggregateQuery,DistinctQuery,MongoCollection,MongoClientandRepositoryhave new abstract
members.containsAll(Nil)matches nothing, as$all: []does on the server.- A
sortonupdateManyis refused rather than dropped by the driver.
Bug fixes
ensureKeyIndexfailed on every repository keyed on_id; it now builds nothing for such a key and answers_id_.- Slicing a field the projection also included returned every field of the document.
- A window over
Accumulator.rawwrote into the document it was given, so reusing it in a$groupfailed. - A failed transaction commit carried a misleading suppressed
IllegalStateException. - The fake wrote a positional path as a field called
$[], accepted auniqueindex on_idand partial indexes the
server refuses, matched documents on$all: [], and gave a replacement upsert a new_idinstead of the
filter's.
Compatibility
Scala 3.9 LTS; 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.- The build fails on any compiler warning.
v4.1.0
mongo4s 4.1.0
The aggregation vocabulary MongoDB added in 5.x–7.0 — the ranking and statistical accumulators, window functions,
$fill, $redact and the concise $lookup — plus bitwise updates and filters. Binary compatible with 4.0.0,
checked by MiMa with no exclusions. Upgrade notes:
COMPATIBILITY.md.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "4.1.0"New features
- Accumulators —
top/bottomandtopN/bottomNrank by aSortand output a field;firstN/lastN,
maxN/minN;medianandpercentilewith the server'sapproximatemethod;stdDevPop/stdDevSamp;
mergeObjects. The statistical ones compile only on a numeric field. - Window functions —
WindowFunction.rank,denseRank,documentNumber,shift,locf,linearFill,
derivative,integral,expMovingAvg/expMovingAvgAlpha, given tosetWindowFieldsbeside the accumulators.
Putting one in a$group, where the server refuses it, does not compile. $fill— by a value, by the last value seen, or linearly between neighbours, per partition, through
FillOutput. The sort-driven methods are refused without asortBy.$redact, withStage.Descend/Prune/Keepfor the expression's answers.Stage.lookupWithKeys—$lookupthat joins onlocalField/foreignFieldand narrows with a sub-pipeline.
Both keys are typed, so joining anObjectIdto aStringis a compile error.$bit—bitAnd/bitOr/bitXor— and the$bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear
filters, on integer fields only throughBitwiseOf.Filter.jsonSchema.- The fake simulates the new accumulators except
median/percentile, and$fill,$bitand the$bits
filters — each pinned by a parity spec against MongoDB 8.2. Window functions,$redact,$jsonSchema,
medianandpercentilethrowUnsupportedOperationExceptionnaming what was asked for.
Bug fixes
- The fake was not safe to share between fibers. Concurrent inserts lost documents or threw, a duplicate
_id
or unique key could get through twice, and concurrent$incs lost updates. Every operation now runs atomically,
so a counter or a work queue claimed throughfindOneAndUpdatebehaves under contention as it does against a
server.
Benchmarks
Re-measured on 4.1.0 against mongo4cats 0.8.0, with the machine, server and every library version stated —
BENCHMARKS.md.
WireCodecto real BSON bytes: 2.4–3.2× faster than eithermongo4catscodec, up to 6.4× less allocation.- Single-document operations against a real MongoDB land level for all six stacks.
- Bulk reads pull ahead:
mongo4catsallocates 2.1–6.0× more onfindAll/findStreamand readsfindAll
1.13–1.35× slower. Itsfind(...).streamon cats-effect runs at 1,626 ops/s againstmongo4s's
3,174 —0.8.0closed most of the gap0.7.18had (392 ops/s), not all of it.
Compatibility
mongodb-driver-reactivestreams5.13.0;kyo1.0.0-RC7.mongo4s-kyoremains outside the binary
compatibility promise while kyo is on a release candidate.Accumulator,Filter,UpdateandStagehave new cases, so an exhaustive match over one of them warns.setWindowFieldstakesWindowFields, the common parent ofWindowOutputandWindowFunction; every existing
call compiles unchanged.Scala 3.9 LTS; building requiresJDK 25. Tested against JDK 25 and MongoDB 8.2.
v4.0.0
mongo4s 4.0.0
Pipeline updates, sessions without a transaction, $geoNear and three more stages, and a testkit fake that answers
the way the server does. Most breaks are compile errors; a few calls now refuse what they used to accept quietly.
Full migration guide: COMPATIBILITY.md.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "4.0.0"Breaking changes
- An
Updatethat changes one path twice is refused when it renders —inc(n, 1).and(inc(n, 2))used to send
{"$inc": {"n": 2}}and lose the first change. Paths that overlap across operators are refused too; the server
refused those anyway. No compile error behind this one. findPagereturnsKeysetPage(items, next)— passnextasafter; it isNoneon the last page.watchAttemptingyieldsChangeEvent[DecodeResult[A]], so an event whose document fails to decode still
arrives, resume token included.watchAsAttemptingon the client and the database changed the same way.distinctover an array answers with its elements —distinct[C, B]throughDistinctOf. A call that named
the type argument names both now, or neither.regex,mod,hasSizeandpopFirst/popLastcompile only on text, numeric and collection fields.- A
limitorbatchSizebelow one and a negativeskipthrow where they are set —FindQuery,
CountOptions,WatchOptions,StageandPage. MongoDB reads alimitof 0 as no limit at all. ChangeEventgainednamespaceanddestination— positional construction takes two more arguments.Updatehas aPipelinecase,Stagefour new cases,MongoCollectionthree new members — an exhaustive
match or an implementation of your own has more to cover.- The testkit fake takes the runtime's
RsBridgeinstead of anemitfunction —
FakeMongoCollection[IO, S, User](codec)— and itslistIndexesdescribes indexes as the server does. WireCodec.spellsAsremoved.
New features
- Pipeline updates —
Update.pipeline(stages*)over$addFields,$project,$unsetand$replaceRoot,
taken byupdateOne,updateMany,findOneAndUpdateand bulk updates. $geoNearwithGeoNearOptions— query, distance bounds, multiplier,includeLocs.$unset,
$sortByCountand$bucketAuto;bucketAutoRoundedcompiles only on a numeric field, the only kind the
server rounds.createIndexes,dropIndexes,renameCollection.MongoClient.withSessionandSessionOptions— a causally consistent or snapshot session without a
transaction, given to every call in the body.- The
findOneAnd*options carrycollation,hint,maxTime,commentandbypassDocumentValidation. findPage(descending = true),Index.expiringAtFieldTime,BaseMongoRepository.createDirect,
BulkWriteFailed.result,Effect.sleep.
Bug fixes
Each reproduced by a failing test first; the fake-versus-server ones are pinned by parity specs against MongoDB 8.2.
- A sum type's codec, and one mapped with
imap/iemap, reported the wrong naming — on a direct collection a
snake_caseenum entity got camelCase filters and updates that matched nothing. emap/iemapover anOptioncodec could not read back the empty value it wrote.- A commit that ran out of
maxCommitTimewas retried until the retry window closed. Whole-transaction retries
now back off with jitter, as the driver specification requires. - A transaction body that threw while being built left the transaction open, and the default
bracketCase—
kyo's and rapid's — released nothing in the same case. - A repository write failing in a later batch named the command by its position in that batch, and its result
left out the batches before it. - Cancelling a
unitcall left its subscription running on cats-effect and kyo. - zio and rapid asked a cursor for a whole buffer to answer
oneoroption. Repository.findOneAndUpdatereplaced your projection; a sub-millisecondmaxTimemeant no limit;Either's codec
let a_typefield overwrite the discriminator;BsonDecoder[BigDecimal]threw on a non-finiteDouble;
Repository.existscounted every match;selectAsrecomputed names and decoders for every document.- The fake answers like the server in many more places — duplicates in
insertManyand unordered bulks, unique
indexes enforced and named as the server names them, streams read when they run, collations refused, numbers
compared across widths, replacements keep their_id, no-op updates not counted as modified, and index creation
and drops failing where the server fails.
Benchmarks
A/B against 3.0.0 on one machine, all six harnesses: no measurable change. Allocations per operation are identical
and every difference sits inside run-to-run error; the driver bump to 5.12.0 is not slower.
BENCHMARKS.md keeps the 3.0.0 measurements, which
stand.
Compatibility
Scala 3.9 LTS; building requiresJDK 25.mongo4s-kyoremains outside the binary compatibility promise while
kyo is on a release candidate.- MongoDB Java driver
5.12.0. Tested against JDK 25 and MongoDB 8.2. - Not binary compatible with
3.x— see the migration guide.
v3.0.0
mongo4s 3.0.0
Whole build on Scala 3.9 LTS, typed MongoError instead of driver exceptions, and WireCodec no longer derives
itself unasked. Full migration guide:
COMPATIBILITY.md.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "3.0.0"Breaking changes
Every item is a compile error, except the transaction retries.
Scala 3.9 LTSrequired,3.3 LTSdropped.TASTydoes not read forward, and MiMa cannot see that break —
which is what takes the major.bson-calypso,kyoandrapidleave their3.8pin and join the LTS line.WireCodecno longer derives itself for any case class it meets. A field whose type has no codec is a compile
error naming the type; anopaque typekeeps its representation.enumcases and sealed leaves still derive with
their parent.- Driver exceptions arrive as
MongoError. Match onMongoError, or oncause, which holds the original. - Compound
PrimaryKeys are named tuples.compound(o => (userId = o.userId, seq = o.seq), snakeCase)replaces
the positional tuple plus its parallel name list.compound3/compound4removed. Repositoryconditions say what they take:findBy→findByField,updateBy→updateByFilter,
deleteBy→deleteByFilter,getBy→getByFilter.Repository.insertOnereturnsK,insertManyreturnsList[K], notOption[BsonValue]/List[BsonValue].getDirectCollectionno longer takes aFieldNaming— the codec decides how queries spell field names.- The direct path reads numbers like the
BsonValuepath — aLongstored asInt32now decodes; a wrong type
isBsonError.TypeMismatch, not a raw driver exception. aggregate[B]takesBsonDocumentDecoder[B], not a codec. Existing calls compile unchanged.$min/$maxtakeComparableOf, so dates, strings andObjectIds work.$inc/$mulstay numeric.WatchOptionsandRsBridgeConfigarefinal classes withwithXbuilders.RsBridgeConfig.Default→
.default;bufferSizemust be positive.Effect.traverse→Effect.flatTraverse.Effectgainedmonotonicwith a default, so implementors are
unaffected.Streamable.instanceisprivate[mongo4s];mongo4s.bson.DecodeResultmoved tomongo4s-bson-core;
createCollectiongained a defaulted options parameter.withTransactionretries —TransientTransactionErrorrestarts the transaction,UnknownTransactionCommitResult
retries only the commit, both under one 120s deadline. No compile error behind this one;
TransactionOptions.withoutRetriesrestores2.x.
New features
MongoError—DuplicateKey,WriteConflict,ExecutionTimeout,Unauthorized,Unavailable,
BulkWriteFailed,Failed. Translated on thePublisher, so every runtime reports the same type;cause,
code,labelsandhasLabelare all there. Costs nothing measurable on the happy path.explainonfindandaggregate, withExplainVerbosity, plusExplainSummary.of— indexes used,
collection scanned, sorted in memory, execution counters. Reads both shapes a plan can take.selectAs[K]—find(...).selectAs[(name: String, age: Int)]builds projection and decoder from one shape.
A wrong label or type is a compile error. Works on direct collections, where partial reads were not possible.aggregateDirect[B]— pipeline output through aWireCodec[B], AST-free on a direct collection.Repository.findPage(limit, after, filter)— keyset paging over the primary key, lexicographic for compound
keys. No re-scan, and deletions do not shift the rows still to come.TransactionOptions— the retry window plusreadConcern,writeConcern,readPreference,maxCommitTime.- Geospatial filters —
near,nearSphere,within,intersectsover GeoJSON plus legacy2dshapes. An
unclosed polygon ring is refused rather than sent. $bucket,$densify,$setWindowFields, with typed boundaries and window vocabulary.CreateCollectionOptions— capped, validators, time-series, clustered,expireAfter, collation. Combinations
the server accepts and ignores are rejected.- Native
WireCodecforBigDecimal,Instant,UUID,ObjectId— noBsonValueper field. Same BSON on
the wire. Repository.getByField/updateByField/deleteByField, each its…ByFiltercounterpart over
field.equalTo(value).- The fake simulates much more —
aggregate($match,$sort,$skip,$limit,$project,$count,
$group) anddistinct, so aggregating service code is unit-testable without Docker. Sort.byTextScore,Projection.slice/sliceFrom,Index.text,Index.geo2D,
WatchOptions.withExpandedEvents,ChangeEvent.wallTime/splitEvent, andkeyon
Filter/Update/Stage/Accumulator.
Bug fixes
Each reproduced by a failing test first; the fake-versus-server ones are pinned by parity specs against a real
MongoDB, and each spec was run against the unfixed fake to confirm it catches the divergence.
- The fake reported an upsert that inserted as doing nothing — no
_idstamped, sowasUpsertedwasfalse
where the server saystrue. The bulk path reported the entity'sidfield, or the document as JSON. - The fake simulated four of the update operators while the docs said updates were simulated. All are simulated
now,$push's$each/$position/$slice/$sortincluded. - The fake answered several queries differently from the server — array filters compared the whole array,
dotted paths through arrays found nothing,$regexanchored and ignored options,$eq nullmissed absent fields,
$incpromotedInt32toInt64,$counton no input returned a row. PublisherIterator.cancelleaked a thread per cancelled or timed-outrapidstream — no terminal was queued,
so a consumer parked inhasNextstayed parked.RsBridge.unitwrapped failures inCompletionExceptionon some backends, hiding the driver's exception type
and the error labels transaction retries depend on.selectAsreportedMissingFieldfor an absentOptionfield, where reading the whole entity gaveNone.Repository.bulkWritehad noorderedflag and always batched, so an unordered bulk stopped at the first
failing batch.@publicInBinaryon the accessors synthesized forField.ofand theWireCodecderivations — binary-unstable in
a way MiMa cannot check.ReadmeReferencesSpecfails if a README snippet names a member the source does not define.
Benchmarks
Re-measured end to end on stated hardware. Six JMH harnesses, mongo4cats as reference —
BENCHMARKS.md.
WireCodecto real BSON bytes: 2.7–3.2× faster than eithermongo4catscodec, up to 6.9× less allocation.- Against a real MongoDB every single-document operation lands inside run-to-run error for all six stacks.
- Bulk reads pull ahead:
mongo4catsallocates 2.0–13× more onfindAll/findStream. - The server-backed rates are bound by Docker's round trip, not by any library on the page — and the same benchmark
on the same server moved 18% between two runs eight hours apart, so anything narrower is noise.
Compatibility
Scala 3.9 LTS, every module.mongo4s-kyoremains outside the binary compatibility promise while kyo is on a
release candidate.- Building requires
JDK 25— the build stops and says so rather than failing onmongo4s-kyowith
UnsupportedClassVersionError. Build-time only; published artifacts are unaffected. - Tested against JDK 25 and MongoDB 8.2. Note
mongo:8(the8.0line) will not start on Linux kernels 6.19+
(SERVER-121912), which includes Docker Desktop's VM;8.2and
7both run.
v2.0.0
mongo4s 2.0.0
Two things drive it. The write API's options moved out of method parameters and into immutable values, which is what
lets every future option ship in a 2.x minor instead of another major. And an independent audit of the codebase
found a set of real defects, all fixed here, each reproduced by a failing test first.
The compiler catches every breaking change — none of them alters behaviour silently. The full migration guide is in
COMPATIBILITY.md.
libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "2.0.0"New features
Read and write concerns per database and collection. withReadConcern, withWriteConcern and
withReadPreference return a new handle rather than mutating the one you hold, so per-operation control is just
chaining: collection.withWriteConcern(WriteConcern.MAJORITY).insertOne(x). Previously these could only be set for
the whole client, and there was no way to build a MongoCollection from a driver collection you had narrowed
yourself.
mongo4s-testkit, a new published module. FakeMongoCollection used to live in mongo4s-repositories' test
sources, which meant nobody outside this build could use it. It is now published, together with a new
FakeRepository — a real BaseMongoRepository over a fake collection, so the repository logic under test is the one
that ships.
arrayFilters and the positional operators. $[] and $[identifier] are ordinary path segments, so
Field.of[Order, List[Item]](_.items) / "$[low]" / "qty" builds them; the filters themselves go in UpdateOptions.
Write, delete and count options. collation, hint and comment on every write, bypassDocumentValidation
where a whole document is written, and limit/skip/maxTime on count.
$each modifiers on pushAll — $slice, $sort, $position, for a capped, ordered array maintained by the
server rather than by the application.
More index types — hashed, 2dsphere, 2d, plus hidden, collation and wildcardProjection. Wildcard
indexes needed no new API: $** is a stored path.
Richer aggregation — $out and $merge take a target database, and $merge its on/whenMatched/
whenNotMatched policies; $lookup gained its sub-pipeline form with let; $graphLookup is new.
omitNoneFields (default on). A None field is left out of the document instead of written as an explicit
null, which is what a consumer asked for after reporting a 2×+ drop in memory footprint. Reads are unaffected in
both directions, so mixed collections keep working.
BsonTypeName is public and an enum, so hasType takes a value rather than a $type alias string that a typo
turns into a filter matching nothing.
API changes
Every item here is a compile error, not a behaviour change.
Write options are values, not parameters. UpdateOptions, ReplaceOptions, DeleteOptions, CountOptions and
the entity-typed FindOneAndUpdateOptions[A] / FindOneAndReplaceOptions[A] / FindOneAndDeleteOptions[A]:
collection.updateOne(filter, update, upsert = true) // 1.x
collection.updateOne(filter, update, UpdateOptions.upsert) // 2.0deleteOne, deleteMany and count gained an options parameter. WriteCommand and Repository follow.
Projection is two types. Mixing inclusion with exclusion no longer compiles, where it used to silently discard
whatever you listed first and return more fields than you asked for. Projection.empty returns
Projection.Everything[E].
Index and WireCodecConfig are no longer case classes. Both are final classes with private constructors and
withX builders, for the same reason: a case class cannot gain a field without breaking apply/copy again in
every release, and options keep arriving.
hasType takes BsonTypeName, and BsonError.TypeMismatch carries it in both fields. The rendered message is
unchanged.
MongoCollection and MongoDatabase gained three abstract methods — the concern setters — which matters if you
implement either trait outside this library. Stage gained LookupPipeline and GraphLookup cases, which matters
if you match on it exhaustively.
FakeMongoCollection moved to mongo4s.testkit, in the new module.
Bug fixes
Each of these was reproduced by a failing test before being fixed; the codec and query fixes are pinned against a
real MongoDB.
- A sealed hierarchy stored as the root entity of a direct collection could be written but never read back. The
decoder required the_typediscriminator to be the document's first field, and the server stores_idfirst.
EitherandIorcarried their own copies of the same logic and the same defect. - Every write threw under an unacknowledged write concern. With
w=0the driver has no result to report, and
each getter raisedUnsupportedOperationException. Results now come back empty. Decimal128lost precision above 2^53 by being read throughDouble; theLongrange check admitted exactly
2^63 and saturated; a non-finite value threwArithmeticExceptionpast theEither.- An explicit
BsonEncoder/BsonDecoderpair and automaticWireCodecderivation were ambiguous, so a case
class carrying both resolved no codec at all. Derivation is now the lower priority. cats.data.Iordecoded its two sides by position, silently swapping them when the stored field order differed
and both sides shared a wire type.SortandIndexkept a repeated field's original position, soasc(a).asc(b).desc(a)sorted byafirst.insertManyreturned ids in the driver's map iteration order rather than by command position.aggregate(...).firstappended$limitafter$out/$merge, which the server rejects.- A projection that mixed inclusion and exclusion silently returned extra fields, and
Projection.excludeId
followed byincludesilently lost the_idexclusion. WatchOptions' three starting points were not mutually exclusive, so a resume token and an operation time
could both reach the server, which rejects that.- A bare discriminator naming a case that carries fields raised
IndexOutOfBoundsExceptioninstead of a
BsonError, and a subtype declaring its own_typefield wrote duplicate BSON keys. - rapid's
MongoClientResourceleaked the client whenusethrew while its task was being built. - zio's
bracketCaseswallowed a finalizer error on success, where every other runtime raises it. - kyo's
streamfailed as aClassCastExceptionwhen handed aStreamableit had not derived itself. - Reads on the direct path are strict about BSON numeric types — an
Int32where the model saysLongis an
error, unlike the lenientgetCollectionpath. This was always true; it is now pinned by tests and documented.
Known gaps
GridFS, geospatial operators, CreateCollectionOptions, transaction retry semantics and a circe bridge are not
covered. Each has a workaround today, and all but one are additive — see
ROADMAP.md.
Compatibility
Scala 3.3 LTS, except mongo4s-bson-calypso, mongo4s-kyo and mongo4s-rapid, which are pinned to 3.8 by their
upstream dependencies and cannot be consumed from a 3.3 project. mongo4s-kyo sits outside the binary compatibility
promise while kyo is on a release candidate. Tested against JDK 25 and MongoDB 7.
v1.1.0
Highlights
bson-directno longer storesnullfor absentOptionfields. A field holdingNoneis left out of the document entirely, so collections with many optional fields get materially smaller on disk. Reads are unaffected in both directions — a missing field decodes toNone, and documents already carrying an explicitnullstill decode toNone— so there is no migration.WireCodecConfigis now a builder: start fromWireCodecConfig.Defaultor.SnakeCaseand chainwithFieldNaming,withDiscriminatorNaming,withEncodeEmptyCasesAsString,withOmitNoneFields. Derivation options can now be added without breaking binary compatibility, so this is the last time the type changes shape.- Binary compatibility is enforced, not just promised. MiMa runs in CI against the previous release; every deliberate exception is a filter in
mima.sbtand an entry inCOMPATIBILITY.md. DESIGN.md— why the library is shaped the way it is: what each decision cost, which sharp edges are deliberate, and what was rejected. Benchmarks and the compatibility policy moved into their own files too.
Breaking changes
WireCodecConfigis no longer acase class… ReplaceWireCodecConfig(fieldNaming = x)withWireCodecConfig.Default.withFieldNaming(x). Source and binary — recompile against 1.1.0.- Documents written through
bson-directno longer contain keys forNonefields.{field: null}still matches them, but$exists: trueand sparse indexes no longer see them;withOmitNoneFields(false)returns the old behavior.