Changelog
Notable changes per release. Breaking changes and how to migrate are in COMPATIBILITY.md; what is not covered yet is in ROADMAP.md.
This file starts at 2.0.0. 1.0.0 was released without one, and 1.1.0 was never published — its changes are
part of 2.0.0 below.
6.0.0
Binary compatible with org.mongo4s 5.0.0 and 5.1.1, checked by MiMa. Only the coordinates change; how to move is in
COMPATIBILITY.md.
Changed
- Published under
now.sendline:"now.sendline" %% "mongo4s-core" % "6.0.0". The package is stillmongo4s. org.mongo4s6.0.0is a relocation tonow.sendlinefor every module, so a version bump alone resolves to the new organization in Maven and in sbt.- The repository moved to sendline-now/mongo4s, and the site to
oss.sendline.now/mongo4s, shared with the other Sendline libraries and built in
its own repository.
docs/is gone from this one, andReadmeReferencesSpecchecks the README only.
5.1.1
Binary compatible with 5.0.0 and 5.1.0, checked by MiMa.
Fixed
- A large reply that reached the client in many small reads hung the call.
mongodb-driver-reactivestreams5.6.0to5.13.0nest one callback per partial socket read of a reply and unwind them all at once when it is complete; about 900 reads overflow the driver's I/O thread, and theStackOverflowErrornever reaches the caller, so thefindwaits forever (JAVA-6313). A link that hands the client a few kilobytes at a time is enough — TLS, or Docker Desktop'shost.docker.internal, where a 3.5 MB reply took over 1,500 reads. The driver is5.14.0now, which runs those reads in a loop.LargeReplyItSpecreads a 3 MB batch through a proxy that passes the server's answer on a kilobyte at a time; on5.13.0it times out with the overflow in the log. - On
kyo,guaranteeCasereported a cancellation as an error. kyo1.0.0-RC7does not resume a fiber interrupted while it waits — on the server, in asleep, inAsync.never— but abandons it, and closes what the fiber owed withKyoException("fiber abandoned")rather than the interrupt. The finalizer sawExitCase.Erroredof that exception instead ofExitCase.Canceled, and so did whatbracketCaseacquired. A transaction still rolled back, sincewithTransactionaborts on either, but code onEffect[KIO]that tells the two apart — metrics, logging — counted every timeout as a failure.guaranteeCasenow records how the body ended, and a scope that closes with nothing recorded is a cancellation, whatever kyo reports.cats,zioandkyoeach have a spec that interrupts a body insideguaranteeCasenow; none did.
Changed
mongodb-driver-reactivestreams5.14.0.
5.1.0
Binary compatible with 5.0.0, checked by MiMa.
Added
- A
ScalarWireCodecis theBsonEncoderandBsonDecoderof its type. Filters, updates, keys,selectAsanddistinctfind it in the type's companion or an import, sogiven BsonEncoder[X] = codec.toBsonEncoderis no longer needed; one written by hand still wins.ScalarWireCodec.toBsonDecoder, andtoBsonCodecon everyWireCodec, document-shaped ones included. BaseMongoRepository.directbuilds a repository overgetDirectCollectionand answers it as aRepository[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. Aprojectionargument does whatwithoutIddoes.PrimaryKey.ofkeys an entity on one, two or three of its own fields —PrimaryKey.of(_.provider, _.providerId)— as a plain tuple, each field spelled through the collection's naming wherever the key renders,ensureKeyIndexincluded.KeyRef.keyPathsis what the key's filters, sort and index are now built from; for a key over stored names it is those names, so existing keys render as before.@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, reading it back as its default orNone, with no codec needed for its type.Field.of,PrimaryKey.ofandselectAsspell a@storedfield the same way, and refuse a@notStoredone. A field with no codec is now reported by name when its class derives.- Sums shaped for collections someone else defined.
WireCodecConfig.withDiscriminatorFieldnames the field that holds the case (_typeby default);withWrappedCaseswrites each case as one field named after it,{"Circle": {"radius": 2.0}};withFallbackCasereads a case it does not know as acase objectof that name, for rolling deployments. - Named scalar representations:
ScalarWireCodec.instantAsEpochMillis,instantAsEpochSeconds,instantAsIsoString,uuidAsBinary, and forLocalDate, which has no default,localDateAsStringandlocalDateAsDate(zone)— each ready to be made the given where a collection already stores the type that way. - A
UUIDstored as binary subtype 4, as every other driver writes it, reads on both paths; a legacy subtype 3 is refused by name. ScalarWireCodec.byValue(values)(_.value), the codec of an enum stored by a value of its own, andWireCodec.decodeOnly(decoder), for a type that is only read.Mapkeyed by more thanString. AKeyCodec[K]writes each key as the field name it has to be —Int,LongandUUIDhave one, and any type whoseScalarWireCodecwrites a string gets one — soMap[Provider, V]derives without a hand-written codec, andfield.at(key)reaches an entry in a query.Field.of[E](_.field)names only the entity and takes the field's type from the selector.- Repository methods services were writing by hand:
findOneByFilter,estimatedCount,updateByKeys(one update for every listed key, batched),updateEach(each key its own update, one bulk write) andupsertKeeping(an upsert that leaves the fields another writer owns as stored), andfindOneAndReplace(entity)andfindOneAndDelete(key). Each has a body onRepository, so a hand-written implementation keeps compiling. FakeRepository.directandFakeMongoCollection.directbuild a fake from aWireCodec, its naming included, andFakeRepositoryisopen, so a service's own repository trait can mix into it.MongoClient.ping(timeout),falsewhen the server does not answer in time, and inmongo4s-catsMongoClientResource.reachability(client, interval), a background ping whose last answer a readiness check reads.BaseMongoRepository.objectIdDirect, and aWireCodecforWithId[Id, E]wheneverEis a derived case class:_idfirst, the entity's fields beside it, under the entity's naming.MongoError.writeConcernErrors, withMongoError.WriteConcernFailure: what the write concern reported, on a single write, a bulk write and a client bulk write.field.setNullandUpdate.setNull(field)on anOptionfield: the explicitnullthatset(None)no longer writes.- Typed paths through an
Optionand into a collection's elements.field / innertakes the inner case class's ownFieldwhenfieldholds it in anOption, a collection, or an optional collection, and spells every segment through the naming —itemsField / skuFieldisitems.sku, whereitemsField / "sku"stays a stored segment the naming never renames. The array operators also apply to anOptionof a collection. - Joins that take the foreign collection.
Stage.lookup,lookupWith,lookupWithKeysandgraphLookupaccept the foreignMongoCollectionin place of its name, and render its fields, sub-pipeline andrestrictSearchWithMatchthrough that collection'sFieldNamingrather than the one running the aggregation, so collections with different namings join on the right fields. The string forms are unchanged. Float,Short,Byte,CharandArray[Byte]on both codec paths, stored as the Java driver stores them — double, int, int, one-character string and binary — with range checks on the way in, and the numeric and comparison operators on such fields.withTimeout(duration)onMongoDatabaseandMongoCollection, the driver's client-side operation timeout for one handle rather than the whole client; an operation that runs out fails asMongoError.ExecutionTimeout. AMongoCollectionorMongoDatabaseof your own inherits a default that refuses it by name.- A
PrimaryKeyforWithId[Id, E]over any id with aBsonEncoder, keyed on_id, beside the one forObjectIdthat was there. $textwith its language and sensitivities:Filter.text(search, TextSearchOptions.default.withCaseSensitive(true)), withwithLanguageandwithDiacriticSensitivebeside it. The filter is a newFilter.TextSearchcase; theFilter.text(search, language)that exists builds theTextcase it always did.
Changed
field.set(None)renders$unset, not$set: null, so a document updated toNonehas the shape the default codec gives one inserted with it —$exists, sparse indexes and$typeno longer depend on which write touched it last.setOnInsert(None)adds nothing to the update, andRepository.updateField(key, field, None)removes the field. Where thenullwas wanted,setNullwrites it. Documents already carrying anullread back asNoneas before.- Decoding a derived entity is cheaper: a product no longer copies its fields into a tuple on the way to its
constructor, and a sum whose
_typeis first reads its case without rewinding — about 1.09× on a consumer-shaped entity, 592 bytes less per document (BENCHMARKS.md). - Rendering a query is cheaper:
snakeCaseandkebabCaserename without boxing aCharper letter, and an update's overlap check no longer splits every path into arrays — 1.5× on a five-condition filter and 2.4× on a ten-operator update, with half the garbage on the update (BENCHMARKS.md). - A
ScalarWireCodecencodes a query value without a writer of its own.toBsonEncoder— theBsonEncodera filter on an opaque id goes through — built a capturing writer, its context and its name stack for every value; it reuses one per thread now, so an$inover a thousand opaque ids renders 2.1× as fast and allocates what one over plain strings does (BENCHMARKS.md). - A
WireCodecdecoding failure names where it happened. Each field, array position, map key, sumvalueandIorside wraps the failure inBsonError.Nested, so the message readsaddress -> city -> Expected string but got intinstead of the leaf alone; code matching a field'sTypeMismatchdirectly now finds it inside aNested. A reader exception for a value of the wrong shape is reported the same way, as aDecodingFailure, rather than as the driver'sBsonInvalidOperationException. - A
BsonDecodercollection names the element that failed.List,Vector,SeqandSetstop at the first element that does not decode and report it under its position,Mapunder its key — they reported the last failure, and a list's without saying where.Vector,SeqandSetno longer build aListdecoder for every value they read, nor aListon the way to their own collection.
Fixed
- The website told users to avoid
getDirectCollectionfor a collection with mixed numeric widths, a limit3.0.0had already removed, and saidaggregatenever reads directly althoughaggregateDirectdoes. Both are corrected, along with its benchmark figures and counts, the claim thatkyoandrapidread the runtime's clock formonotonic(they keepSystem.nanoTime), the README'sMongoCollectionsketch, thebson-cats-dataexample (which could not compile, and now is compiled), and what the fake ignores rather than refuses. The README check covers its prose and the website now, not only its code blocks. - The fake failed on a field holding
null, values of mixed types or an array in a range filter, a sort, an$min/$maxupdate or a group's$min/$max, throwingcannot comparewhere the server answers — and every codec but the direct one writesNoneasnull. It compares in BSON's type order now, filters element by element through arrays and by the operand's type, and sorts an array by its extreme element; each case is checked against MongoDB 8.2. imaporiemapon a derived case class moved its fields undervaluewhen that class was a case of a sealed trait or a branch of anEither— so wrapping it to validate what it reads, as the README suggests, changed how existing documents had to look. The mapped codec keeps the class's shape now. Documents already written in the nested shape by such a codec still read throughWireCodec.from(codec, codec), which keeps it.- A field whose codec reads
nullas a value was reported missing. Derived products told an unread field from a read one by anullin the decoded slot; they use a marker of their own now. - A
_typethat is not a string was the reader's own exception. It is aTypeMismatchunder_type, like any other value of the wrong type. - A sealed trait or an enum with more than 32 cases did not derive without raising
-Xmax-inlinesin the user's build: the cases' codecs were gathered by an inline method that recursed once per case. A macro gathers them now, so the size of a sum no longer matters; products were never affected. MongoErrormisfiled the failures operations teams alert on. An operation whose clienttimeoutMSran out wasUnavailable, because the driver'sMongoOperationTimeoutExceptionextends the server-selection timeout; it isExecutionTimeoutnow, andwithTransactionno longer re-commits a commit that ran out of it. Refused credentials (MongoSecurityException) wereFailedand areUnauthorized. A primary that stepped down, a node recovering, a cleared connection pool and a closed server wereFailedand areUnavailable.- A bulk write that failed only on its write concern raised a
BulkWriteFailedwith nothing in it.failuresis still empty — no document failed — andwriteConcernErrorsnow says what did. - A filter that matches nothing was planned as a collection scan.
in(Nil),containsAll(Nil),Filter.noneand an emptyKeyRef.inFilterrendered{$nor: [{}]}, which the server reads every document to answer. They render{_id: {$in: []}}now, answered from the_idindex.Filter.Or(Nil), which rendered an$orthe server refuses, and!Filter.allrender the same way, and!Filter.nonerenders{}. A partial index accepts all of them, in the fake as on the server. where(Field.self)rendered$[], updating every element instead of the ones an array filter selects.wheretakes only an array filter identifier now, asField.elementbuilds it, and refuses anything else when the path is built.- A date's
plusandminustook only aLongamount, soExpr.lit(1)or anIntfield did not compile; the server takes either, and so do they. - Some refusals were thrown where the operation was built, not raised in
F, soattemptandhandleErrorWithnever saw them: an aggregation or adistinctwhose filter does not render (an unclosed polygon ring), create options that contradict each other, afindPageof no entities, aCounters.nextstep that does not move forward, and the driver's own argument checks onlistIndexes,dropIndex,dropIndexes,drop,runCommand,dropCollectionand the listing streams. All of them fail the effect now, and a kyo stream builds its publisher when it runs rather than when it is defined. Accumulator.sumandavgover a field took any field, and the server answers0for a sum of strings. They compile only on a numeric field or anOptionof one now, asmedianand the standard deviations already did; code compiled against5.0keeps linking, since the unconstrained methods are still in the bytecode.- On
zio,attemptandhandleErrorWithrecovered a defect as an error, so aZIO.diein a transaction body leftwithTransactionas a typed failure. Only failures are recovered now; a defect still rolls the transaction back and then stays a defect. A function given tomaporflatMapthat throws, and asuspendwhose body throws, fail the effect rather than die, as they do on the other three runtimes — each runtime's spec checks it. - On
kyo, a session could be left open by an interrupt. One landing just aswithSessionorclient.withTransactionhad started its session skipped the release, and one landing after a body succeeded could skip aguaranteeCasefinalizer.bracketCaseinstalls its guard before it acquires, and acquires where an interrupt cannot reach; every finalizer runs inside kyo's scope. - The README did not say that transactions do not nest. A
client.withTransactioninside another is a transaction of its own, which the outer rollback leaves alone, and asession.withTransactionon a session already in one is refused; both are said now, and pinned against MongoDB 8.2. - The fake answered several filters differently from the server. A numeric path segment (
items.0.name) was read as a field called0;$size,$type,$mod,$elemMatchand the$bitsoperators did not look through an array of documents, and$type,$modand$bitsnot into an array of scalars; a path went on into an array nested in an array, which the server does not;$alldid not match a scalar field; a regular expression inside$innever matched; and strings sorted by UTF-16 rather than by their UTF-8 bytes. Each is checked against MongoDB 8.2 now. - The fake answered several updates differently from the server. A path through an array position
(
items.0.qty) failed with the driver's own exception; one running into a value that is neither a document nor an array, and$inc/$mulof a non-number, failed the same way rather than with the server's codes 28 and 14; a negative$positionprepended instead of counting from the end;$pullcompared a document for equality instead of reading it as a query on each element;$pull,$pullAlland$addToSettold1from1L; an insert stored a generated_idlast; and a replacement identical to the stored document counted as modified. Each is checked against MongoDB 8.2 now. - The fake answered several reads and bulk writes differently from the server. A projection or
distinctover a dotted path through an array dropped the field;distinctand$grouptold1,1Land1.0apart;firstdecoded every match, so a later document the codec could not read failed it; a bulk write error other than a duplicate escaped as a bareMongoErrorinstead of aBulkWriteFailed; andexplainandwatchthrew where they were called rather than in the effect or the stream. Each is checked against MongoDB 8.2 now. - The README said an
Eitherwith indistinguishable branches is refused when its codec is summoned. For a field of a derived case class it is refused on the first encode or decode of that class, since field codecs are built lazily; the README says so now, and names the opaque-type and anonymous-class cases beside the erased generics. findManyreturned a document once per batch its key appeared in. A key repeated further down the list thanbatchSizewent out in a second$in, and the document came back twice. The keys are made distinct before they are batched, as the README already promised.
5.0.1
Binary compatible with 5.0.0, checked by MiMa.
Fixed
- 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. Both build their pairs as tuples now.
Changed
- The integration tests compile on Scala
3.10against the library built by3.9, so what the library inlines is checked on the compiler its users are moving to.
5.0.0
A breaking release, mostly in binary shape: most existing calls compile unchanged. The migration notes are in COMPATIBILITY.md.
Added
- Typed aggregation expressions.
Expr[E, A]computes a value from a document —field.expr,Expr.lit, and arithmetic, comparisons, logic,cond/switch, text, array (filter/mapover a typed element), date and conversion operators, each compiling only where the server would accept it.$addFieldstakes computed fields ("total" := price.expr * qty.expr, or aFieldtarget spelled through the naming), and so do$groupkeys (groupByExpr,groupByKeys), accumulators (Accumulator.sum(expr)and the rest),$replaceWith,$redact, an inclusionProjection(compute, infindas well as in$project),Filter.exprand pipeline updates. - Atlas Search.
$searchand$searchMetaover typedSearchOperators —text,phrase,autocomplete,wildcard,regex,equalTo, ranges,exists,compound— and$vectorSearch, approximate or exact, with aFilterpre-filter;createSearchIndex(es),updateSearchIndex,dropSearchIndexandlistSearchIndexes, withSearchIndex.dynamic,SearchIndex.ofand typed vector indexes. Score withExpr.searchScore. Checked against amongodb-atlas-localcontainer. - The client-level
bulkWrite(MongoDB 8.0+): each collection'swrite(command)addresses aWriteCommandto its namespace with its own codec, andclient.bulkWritesends writes to several collections and databases in one command, failing asMongoError.ClientBulkWriteFailed. - Typed positional updates.
field.each($[]),field.matched($) andfield.where(element)($[id]), withField.elementbuilding both the path and its array filter, and/with aFieldreaching into the element through the naming. pullWhereremoves array elements by a condition, andField.selfstates a condition on a scalar element itself — forpullWhereand for a range inside$elemMatch.- Insert options:
InsertOneOptionsandInsertManyOptionswithcommentandbypassDocumentValidation, andInsertManyOptions.unordered, which carries on past a duplicate. The repository'sinsertManytakes them too. letvariables on every write, onfindand onaggregate;sortonupdateOne/replaceOne(8.0+);commentoncount; options on the bulk delete commands.- Cursor options:
cursorType(a tailable cursor streams as a live stream),maxAwaitTime,noCursorTimeoutandallowDiskUseonfind;maxAwaitTimeandbypassDocumentValidationonaggregate;hintandcommentondistinct;EstimatedCountOptions. $typeover several types (hasAnyType),$currentDateas a timestamp (Update.currentTimestamp),$unwind'sincludeArrayIndex,$unionWithwith a pipeline, and a text index's weights, default language and language override.insertIfAbsenton the repository inserts an entity only when its key is free and answers whether it did, through an upsert that only writes on insert, so a taken key neither overwrites nor fails a transaction.replace(entity, condition)on the repository replaces by key, never inserts, and only while the condition holds — the optimistic-concurrency write.- Counters:
MongoCounters.create(database, name)givesnext,currentandresetover one document per counter,nextbeing one atomicfindOneAndUpdate;FakeCountersin the testkit. WireCodecConfig.withDecodeMissingAsDefaultfills a missing field from the case-class default, for documents written before the field existed. It is off by default.- The fake simulates unordered inserts, a
sorton single-document updates,pullWhere, conditions on scalar elements,$typeover several types and a timestamp$currentDate. - The fake simulates update-based upserts —
updateOne,updateMany,findOneAndUpdate,findOneAndReplaceand thebulkWriteupdate commands — building the inserted document from the filter's equalities the way the server does, checked against it case by case. A replacement upsert takes its_idfrom the filter.
Changed
insertOne/insertManytake options, andbulkWritetakesBulkWriteOptionsinstead ofordered—bulkWrite(commands, BulkWriteOptions.unordered)— on the collection and on the repository.Stage.addFieldstakes computed fields —"name" := expr— rather than(String, BsonValue)pairs.- AST cases changed shape:
Stage.Grouptakes aGroupKey,Filter.ExprandStage.RedactanExpr,Filter.Typea list of types,Update.CurrentDatewhether it writes a timestamp,Projection.Includeits computed fields,Stage.UnwindincludeArrayIndex,Stage.UnionWitha pipeline, andWriteCommand.DeleteOne/DeleteManytheirDeleteOptions. FindQuery,AggregateQuery,DistinctQuery,MongoCollection,MongoClientandRepositoryhave new abstract members.containsAll(Nil)matches nothing, as$all: []does on the server — it matched everything before.- A
sortonupdateManyis refused, where the driver drops it without a word. - The build fails on any compiler warning.
zio2.1.27, Testcontainers2.0.5, sbt2.0.11, MiMa1.2.1.
Fixed
ensureKeyIndexfailed on any repository keyed on_id—objectId,WithId,storedId— since the server refuses auniqueflag on the_idindex. It now builds nothing for such a key and answers_id_.- Slicing a field the projection also included dropped the inclusion, and the server returned every field. The sliced field now keeps the projection in inclusion mode, and the one combination the server cannot express is refused.
- A window over
Accumulator.rawwrotewindowinto the document it was given, so reusing it in a$groupfailed on the server. - A transaction whose commit failed carried a misleading suppressed
IllegalStateExceptionfrom aborting a transaction the commit had already ended. - The fake wrote a positional path as a field called
$[]; it refuses positional updates by name now. It also accepted auniqueindex on_idand partial indexes the server refuses, and matched documents on$all: [].
4.1.0
Binary compatible with 4.0.0, checked by MiMa.
Added
- The 5.x and 7.0 accumulators.
top/bottomandtopN/bottomNrank by aSortand output a field;firstN/lastN,maxN/minN;medianandpercentile, always with 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. They cannot be put in a$group, where the server refuses them. $fill— by a value, by the last value seen, or along the line between neighbours, per partition, throughFillOutput. The two sort-driven methods are refused without asortBy.$redact, withStage.Descend/Prune/Keepfor the expression's answers.Stage.lookupWithKeys— the concise$lookupthat joins onlocalField/foreignFieldand narrows with a sub-pipeline. The keys are typed on both sides.$bit(bitAnd/bitOr/bitXor) and the$bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClearfilters, on integer fields only throughBitwiseOf;Filter.jsonSchema.- The fake simulates the new accumulators except
median/percentile, and$fill,$bitand the$bitsfilters — each checked against MongoDB 8.2 by a parity spec.
Changed
setWindowFieldstakesWindowFields, the common parent ofWindowOutputandWindowFunction; every existing call compiles unchanged.mongodb-driver-reactivestreams5.13.0;kyo1.0.0-RC7.- An
.editorconfigtrims trailing whitespace, which is what turned the format check red. - The benchmarks are re-measured against
mongo4cats0.8.0(andcirce0.14.17, whose decoders now answer anEither), on macOS 26.7.1 and Docker Desktop engine 29.8.2, with the version of everything compared named in BENCHMARKS.md. The environment moved more than the libraries did: a round trip to the server is 0.135 ms where the old tables had 2.4 ms, so the server-backed rates are over 20× higher and the bulk-read gaps betweenmongo4sandmongo4catsare now visible.mongo4cats'find(...).streamoncatsis ~4× faster in0.8.0than in0.7.18, measured side by side, and still about half ofmongo4s's.
Fixed
- The fake is safe to share between fibers. Its storage was a bare
ArrayBuffer, so concurrent inserts lost documents or threw, a duplicate_idor unique key could get through twice, and concurrent$incs lost updates. Every operation now runs under one lock.
4.0.0
Added
- Pipeline updates.
Update.pipeline(stages*)computes from the document's own values —$addFields,$project,$unset,$replaceRootor aStage.raw— and is taken byupdateOne,updateMany,findOneAndUpdateand a bulkUpdateOne/UpdateMany.Update.Rawcould not carry one, since a pipeline is a list of stages rather than a document. $geoNear,$unset,$sortByCountand$bucketAutostages.Stage.geoNearsorts by distance and writes it into a field you name, withGeoNearOptionsfor a query, distance bounds, a multiplier andincludeLocs.Stage.unsetdrops fields, one and then more withand.bucketAutoRoundedrounds$bucketAuto's boundaries to aBucketGranularityseries, and compiles only on a numeric field, the only kind the server rounds.createIndexes,dropIndexesandrenameCollectiononMongoCollection.createIndexessends one command and returns the names in the order given;renameCollectionrefuses to replace an existing collection unlessdropTargetis set.MongoClient.withSessionandSessionOptions. A session without a transaction — causally consistent, or a snapshot that reads one point in time — given to every call in the body and closed however it ends.startSessiontakes the same options.- The
findOneAnd*options carrycollation,hint,maxTimeandcomment, and the update and replace onesbypassDocumentValidation, which the plain writes already had. ChangeEvent.namespaceanddestination— the database and collection an event happened in, and where a rename moved it. At the database and client level this is how an event says which collection it came from.Index.expiringAtFieldTime, aTTLof zero asked for on purpose: each document expires at the time its indexed field holds.BaseMongoRepository.createDirect, thegetDirectCollectioncounterpart ofcreate.findPagewalks backwards withdescending = true.MongoError.BulkWriteFailed.result, what the writes that did land wrote.Effect.sleep, with a default implementation and the runtime's own scheduler on all four backends.
Changed
- An
Updatethat changes one path twice is refused when it renders, rather than keeping only the last change — one operator document holds one change per path. So is one whose paths overlap across operators, which the server refused anyway. The fake refuses the same updates. findPagereturns aKeysetPage(items, next).nextis the key the following page starts after, orNoneon the last page; the repository reads one row past the page to know which.watchAttemptingyieldsChangeEvent[DecodeResult[A]]instead ofDecodeResult[ChangeEvent[A]], so an event whose document does not decode still arrives, with its resume token, and a consumer can checkpoint past it.distinctover an array field answers with its elements' type, which is what the server answers with; it was typed as the array and failed to decode.regex,mod,hasSizeandpopFirst/popLastcompile only on the fields they apply to — text, numbers, collections — rather than rendering something the server accepts and that does nothing.- A
limitorbatchSizebelow one and a negativeskipare refused where they are set. MongoDB reads alimitof 0 as no limit at all. FakeMongoCollectionandFakeRepositorystream through the runtime'sRsBridgeinstead of taking anemitfunction. Streams read the collection when they run, andaggregate,distinctandselectAsstream too.MongoCollectionhas three new abstract members andStagefour new cases, so an implementation of your own and an exhaustive match over stages have more to cover.- The fake's
listIndexesdescribes each index the way the server does —v,key,nameand its options, after the_id_index — rather than listing bare key documents. - A chain of
&&or||renders as one flat$and/$or. WireCodec.spellsAsis removed; nothing had asked it sincegetDirectCollectionstopped taking a naming.
Fixed
- A sum type's codec reported identity naming whatever it wrote, and a codec mapped with
imap/iemapdropped the naming of the one underneath. On a direct collection the codec decides how queries spell fields, so asnake_caseenum entity, or an entity codec wrapped iniemapto validate what it reads, got camelCase filters and updates that matched nothing. emap/iemapover anOptioncodec could not read what it wrote: the empty value was left out of the document, and reading the missing field back failed.- A repository write that failed in a later batch named the failing command by its position in that batch, and
its partial result left out the batches before it. The failure now describes the whole write, as the driver does
for its own batches — for
bulkWrite,insertManyandupsertMany. - A transaction commit that ran out of
maxCommitTimewas retried until the retry window closed, because the driver labels it as possibly committed. It is reported instead, and whole-transaction retries pause with jitter between attempts, as the driver specification requires. - A transaction body that threw while being built left the transaction open; it is aborted. The default
Effect.bracketCase— which kyo and rapid use — released nothing whenusethrew the same way. - Cancelling or timing out a
unitcall left its subscription running on cats-effect and kyo. Repository.findOneAndUpdatereplaced the projection you passed with itsdefaultProjection.- zio and rapid asked a cursor for a whole buffer to answer
oneoroption; they ask for the two rows those read, as cats-effect and kyo do. - A
maxTime,maxCommitTimeormaxAwaitTimeunder a millisecond truncated to 0 — formaxTime, no limit at all. It is refused where it is set. Either's codec let a branch with a field named_typeoverwrite the discriminator, and wrote a document it could not read back. It refuses the branch, as a derived sealed trait does.BsonDecoder[BigDecimal]threw on a non-finiteDoubleinstead of returning aLeft.- The document codec bridge caught fatal errors and interruptions along with decoding failures.
selectAsrendered its field names and looked up their decoders again for every field of every document.Repository.existscounted every match; it stops at the first.FakeMongoCollection: a duplicate within oneinsertManywas stored twice, an unorderedbulkWritestopped at its first duplicate, and either reported aDuplicateKeywhere the server reports aBulkWriteFailednaming the command; a unique index was recorded and not enforced, and named differently from the server's, sodropIndexmissed it; a stream was fixed when it was built; afindOneAnd*that could not decode its document answeredNone; a collation was ignored; numbers compared throughDouble, so largeLongs tied and5Ldid not equal5; a replacement dropped the_idit replaced; an update that changed nothing counted as modified; creating an index that already existed recorded it twice, and one that clashed with an existing index's name or keys was accepted; anddropIndexof an index that was not there, or of_id_, did nothing. Each is now checked against MongoDB 8.2 by a parity spec.
3.0.0
Added
Repository.getByField,updateByFieldanddeleteByField, completing the grid the renames above opened up: every condition-based operation now has both a field-and-value and a whole-Filterspelling. Each is its…ByFiltercounterpart overfield.equalTo(value).WireCodecno longer derives itself for any case class it meets. It comes fromderives WireCodec, from agivenyou wrote, or from an existingBsonEncoder/BsonDecoderpair. A field whose type has no codec is a compile error naming the type, rather than a silently derived codec — and anopaque typekeeps the representation you give it. The cases of anenumand the leaves of a sealed trait still derive with their parent, because the language gives them nowhere to writederives.Filter,Update,StageandAccumulatorexposekey, the MongoDB operator each case renders as, declared on the case instead of repeated intoBson.Sort.byTextScore—{"$meta": "textScore"}, so a$textsearch can be ranked by relevance.Sortnow carries aSortOrderper field instead of aBoolean.Projection.sliceandProjection.sliceFrom—$slice, chainable onto a neutral, inclusive or exclusive projection, and refused at compile time on a field that is not an array. Simulated by the fake.Index.textandIndex.geo2Don the companion, which had shortcuts for every other direction but these two.ExplainSummary.of, which reads the questions worth asking out of an explain document — indexes used, stages present, whether it scanned the collection or sorted in memory, and the execution counters. It walks for field names rather than a fixed path, so one summary covers a find, a pipeline the server optimised into a find, and a pipeline it kept as stages.explainonFindQueryandAggregateQuery, taking an optionalExplainVerbosityand returning the server's plan as aBsonDocument. It explains the query the builder already produced, and the whole pipeline rather than the$limit-ed formfirstsends. The fake refuses it by name.MongoError, a typed failure for everything the server reports:DuplicateKey,WriteConflict,ExecutionTimeout,Unauthorized,Unavailable,BulkWriteFailedand aFailedcatch-all that keeps the server's code. Translation happens on thePublisher, so every runtime and every bridge method —one,option,list,unit,stream— reports the same type for the same failure, and the driver's exception stays reachable ascause.FakeMongoCollection.distinctis simulated, array fields flattened into their elements as the server does, and checked against a real server by a parity spec.FakeMongoCollection.aggregatenow simulates$match,$sort,$skip,$limit,$project,$countand$group(with$sum,$avg,$min,$max,$first,$last,$push) in memory, so service code that aggregates is unit-testable without Docker. Anything outside that subset still throws by name,$addToSetincluded — MongoDB leaves the order of its result undefined. A parity spec checks the subset against a real server, BSON types and all.ScalarWireCodecBenchmark,AggregateBenchmarkandErrorTranslationBenchmark, and the numbers they produced, in BENCHMARKS.md: what a bridged scalar costs, whataggregateDirectis worth against a real server at ten documents and at ten thousand, and what theMongoErrorwrapper costs on a publisher that never fails — nothing measurable, at one element and at ten thousand alike.- Native
WireCodecinstances forBigDecimal,Instant,UUIDandObjectId. Before this, the AST-free path had them only through theBsonEncoder/BsonDecoderbridge — oneBsonValueper field, on types almost every entity carries. The BSON written is unchanged, so existing collections read and write exactly as before. TransactionOptions, carrying a transaction'sreadConcern,writeConcern,readPreferenceandmaxCommitTime— and the retry window below. BothwithTransactionmethods take one.Repository.findPage(limit, after, filter): keyset paging over the primary key. The repository builds the sort and the comparison that steps past the cursor itself, lexicographically for a compound key. Unlikeskipit does not re-scan, and a row deleted earlier does not shift the rows still to come.MongoCollection.aggregateDirect[B], which decodes a pipeline's output through aWireCodec[B]rather than aBsonDocumentCodec[B]. On a direct collection that is AST-free, the same tripfindmakes there; on any other collection the codec is bridged, so the call means the same thing either way. It carries the direct path's numeric strictness with it.FindQuery.selectAs[K], a partial read into a named tuple:find(...).selectAs[(name: String, age: Int)]builds the projection and the decoder from one shape, so they cannot drift. A label that is not a field of the entity, or one asked for at the wrong type, is a compile error. It works on a direct collection too, where a partial read was not possible before, because the entity'sWireCodecdemands every modelled field.- Geospatial filters:
near,nearSphere,withinandintersectson aField, over a GeoJSONGeometryvocabulary (Point,LineString,Polygon, and theMulti*forms) plus the legacy2dshapes inGeoShape. A polygon ring that does not close is refused rather than sent. Stage.bucketBy,Stage.densifyandStage.setWindowFields—$bucket,$densifyand$setWindowFields, withDensifyRange/DensifyBounds/DateUnitandWindow/WindowBound/WindowOutputfor the vocabulary they need.$bucket's boundaries are values of the field being grouped rather than raw BSON.CreateCollectionOptions, taken byMongoDatabase.createCollection: capped size and document cap, a validator with its level and action, time-series and clustered collections,expireAfter, collation and storage engine. Combinations the server accepts and then ignores — a document cap withoutcapped, a validation level without a validator — are rejected rather than sent.WatchOptions.withExpandedEvents— the driver'sshowExpandedEvents, so DDL events arrive alongside document ones. Needs MongoDB 6.0, and the flag is sent only when asked for.ChangeEvent.wallTimeandChangeEvent.splitEvent: the server's wall clock for the change, and which fragment of how many an oversized event was split into.Effect.monotonic, with a default readingSystem.nanoTime, so implementors are unaffected;mongo4s-catsandmongo4s-ziooverride it with their runtime's own clock.
Changed
Repository's condition-based methods say what they take.findByisfindByField,updateByisupdateByFilter,deleteByisdeleteByFilterandgetByisgetByFilter.Byused to mean a field infindByand a filter inupdateBy/deleteBy/getBy, while the filter-taking read wasfindByFilter— so the same idea had two names and one name had two meanings.Repository.insertOnereturnsKandinsertManyreturnsList[K], instead of the driver'sOption[org.bson.BsonValue]andList[org.bson.BsonValue]. A repository is parameterised on its key type and can name the key itself throughPrimaryKey, so it never had a reason to hand back raw BSON for the caller to decode; the keys come back in insertion order, across batches.MongoCollection, which has no key type, still returnsInsertOneResult/InsertManyResultexactly as the driver reports them. Code that ignored the result — which is how the result was used everywhere it appeared — needs no change; code that decoded theBsonValueshould drop the decode.- The direct path reads numbers the way the
BsonValuepath does. ALongfield stored as anInt32, aDoublestored as a wholeInt64, and every other lossless width now decode throughgetDirectCollectionas they always did throughgetCollection; what would lose information is still refused on both. The rule lives inBsonDecoderalone and the wire codecs defer to it. A wrong type is now aBsonError.TypeMismatchrather than a raworg.bson.BsonInvalidOperationException, andBsonError.fromThrowableunwraps aDecodingFailureinstead of re-wrapping it asThrown. - Driver exceptions no longer reach your code from an operation.
com.mongodb.MongoWriteExceptionand friends arrive asMongoErrorinstead; code that caught the driver's types directly has to match onMongoError, or oncause, which still holds the original. Errorsmongo4sraises itself —BsonError.DecodingFailure,RsBridgeError— are unchanged. RsBridgeConfigis afinal classwith a private constructor andwithXbuilders, andRsBridgeConfig.Defaultis nowRsBridgeConfig.default— the same shape every other options type already had, and the last one that could not gain a field without a major.bufferSizemust now be positive.JDK 25is now required to build, and the build says so before compiling anything instead of failing on the fifth module withUnsupportedClassVersionError: class file version 69.0.mongo4s-kyosets the floor — kyo'sFramemacro runs inside the compiler and emits Java 25 class files — and the whole build follows it rather than publishing artifacts compiled against different JDKs. The published class files still target Java 17, except thatmongo4s-kyo, like kyo itself, needs Java 25 at runtime.Scala 3.9 LTSis now required, and it is the only Scala version the build uses.mongo4s-bson-calypso,mongo4s-kyoandmongo4s-rapidwere pinned to a fast-release3.8because their dependencies needed a compiler newer than3.3 LTS; they are now on the LTS line with every other module. It takes a major version becauseTASTydoes not read forward, so a3.3 LTSproject cannot consume these artifacts, and MiMa cannot see that break.3.3 LTSis no longer supported.withTransactionretries the way the driver's own does: aTransientTransactionErrorrestarts the whole transaction, anUnknownTransactionCommitResultretries only the commit, both bounded by one 120-second deadline taken before the first attempt.2.xreported the first failure;TransactionOptions.withoutRetriesrestores that. This is the one change in the release with no compile error behind it.- Compound
PrimaryKeys are named tuples:PrimaryKey.compound(o => (userId = o.userId, seq = o.seq))replaces the positional tuple plus its parallel list of names and_._1/_._2extractors. Any width works, socompound3andcompound4were removed. MongoDatabase.createCollectiontakes aCreateCollectionOptions, defaulted, so the no-options call is unchanged at the source level.WatchOptionsis afinal classwithwithXbuilders instead of acase class— same reasonIndexandWireCodecConfigalready were. Build fromWatchOptions.default[E]; reading its fields is unchanged.- The accessors the compiler synthesizes for
Field.ofand the twoWireCodecderivations are pinned with@publicInBinary. They were binary-unstable — a name MiMa cannot check because it is synthesized, not declared. mongo4s-catsbuilds againstfs2 3.14.0. Its Reactive-Streams interop is unchanged from3.13.0— the module is identical between the two releases — so nothing about streaming behaviour moves with the bump.- Internally, every
givenmoved to the syntax SIP-64 introduced in3.6(given name: [A] => (dep: D) => T), so the whole codebase compiles under-source:future. This changes how instances are declared, not what they are, and nothing about it is visible to a caller.
Fixed
FakeMongoCollectionreported an upsert that inserted as if it had done nothing.replaceOnewithupsert = truestored the replacement without stamping an_id, soupsertedIdcame backNoneandwasUpserted/wasAppliedcame backfalsewhere the server returns the new id andtrue— a test asserting onwasUpsertedpassed against the fake and inverted against a real server. The bulk path was worse: it reported the entity's ownidfield, or the whole document as JSON when there was none, instead of the_id. Both now go through the same_idstamping and duplicate checkinsertOneuses, as does abulkWriteinsert, which was skipping it too. A parity spec pins all four against MongoDB 8.2.Streamable.instanceisprivate[mongo4s]. It satisfied anyStreamable[S, A], so a hand-writtengivenfor kyo's stream type compiled and then threw at runtime, because kyo's bridge needs the instance it derives itself. A runtime module still supplies one for its own stream type; nothing else can forge one.Effect.traverseisEffect.flatTraverse: its function returnsF[List[B]]and the result is flattened, which is not whattraversemeans anywhere else.mongo4s.bson.DecodeResultmoved frommongo4s-coretomongo4s-bson-core, so themongo4s.bsonpackage lives in one artifact instead of being split across two.$minand$maxwere gated onNumericOf, solastSeen.max(now)on anInstantfield did not compile although MongoDB orders dates perfectly well. They take a newComparableOf— numbers,String,Instant,ObjectId,UUID,Boolean,BigDecimal, and theirOptions.$incand$mulremain numeric.aggregate[B]asked for aBsonDocumentCodec[B]while only ever decoding, so a read model had to carry an encoder that nothing called. It takes aBsonDocumentDecoder[B]now, likewatchAsbeside it.- A
snake_case(or otherwise renamed)WireCodecand the queries against its collection could disagree about field names, and every query then matched nothing.getDirectCollectionno longer takes anamingat all: a derived codec reports theWireCodecConfigit was built with, a hand-written one declares its own by overridingfieldNaming, and the queries follow whichever it is. selectAsreportedMissingFieldfor anOptionlabel whose field the document did not carry, while reading the whole entity gaveNonefor the very same document. The absent value now goes to the decoder, which is what makes the two agree; a field the entity requires is still reported as missing.PublisherIterator.cancelcancelled the subscription but left no terminal in the queue, so a consumer already parked inhasNextstayed parked — a leaked thread per cancelled or timed-outrapidstream. It now enqueues the end of the stream, and a spec asserts the parked consumer is released.ReadmeReferencesSpecreadsREADME.mdand fails if a Scala block names anObject.memberthe source does not define. The compiled snippets underexamples/are re-typed copies, so they could be right while the README was wrong — which is exactly howIndex.geo2dspheresurvived beside thegeo2DSpherethat exists.Repository.bulkWritehad noorderedflag, although the collection underneath does, and it always batched — so an unordered bulk stopped at the first batch that failed rather than applying everything it could. It takesorderednow, batches only when ordered (where batching preserves the meaning), and sends an unordered bulk as one command.FakeMongoCollectionsimulated four update operators out of eighteen while the documentation said updates were simulated, so a unit test reaching forpush,pull,pop,rename,mul,min,max,addToSetorcurrentDatefailed withUnsupportedOperationException. All of them are simulated now,$push's$each/$position/$slice/$sortmodifiers included, andFakeUpdateParityItSpecchecks each against a real server.FakeMongoCollectionanswered several queries differently from the server. A filter over an array field compared the whole array to the value, socontainsnever matched and$nealways did; a dotted path through an array of documents resolved to nothing;$regexanchored the whole string and dropped its options;$inctruncated its amount to aLongand rewrote the field asInt64, after which the fake could not find the row it had just updated;$countreported0where the server reports nothing;findOneAndUpdate/Replace/Deleteignored theirsort;$eq/$neagainstnulldisagreed with the server about a missing field; sorting or comparing a date,ObjectIdor boolean threw;$first/$lastskipped documents missing the field instead of taking the first or last; a bulkUpdateOne/UpdateManysilently discarded itsUpdateOptions; inserts neither stamped an_idnor refused a duplicate one. Every one of these is now checked against a real MongoDB byFakeFidelityParityItSpec.RsBridge.unitreported a publisher's failure wrapped in aCompletionExceptionon backends that do not unwrap one themselves, which hid the driver's own exception type — and with it the error labels the new transaction retries depend on. It now reports the exception the publisher raised, on every backend.
2.0.0
Added
- Read and write concerns per database and collection:
withReadConcern,withWriteConcern,withReadPreference, each returning a new handle, so per-operation control is chaining. mongo4s-testkit, a new published module carryingFakeMongoCollection— previously unpublished test sources — and a newFakeRepository, a realBaseMongoRepositoryover a fake collection.arrayFilterson the update operations, with$[]and$[identifier]built as ordinary path segments.collation,hintandcommenton every write;bypassDocumentValidationwhere a whole document is written;limit,skipandmaxTimeoncount.$eachmodifiers onpushAll:$slice,$sort,$position.- Index types
hashed,2dsphereand2d, plushidden,collationandwildcardProjection. $outand$mergetake a target database, and$mergeitson/whenMatched/whenNotMatchedpolicies.$lookupin its sub-pipeline form withlet, and$graphLookup.WireCodecConfig.omitNoneFields(default on): aNonefield is left out rather than written asnull.BsonTypeNameis public and an enum.
Changed
Every item is a compile error rather than a silent behaviour change. See COMPATIBILITY.md for the migration.
- Write options moved from method parameters into values:
UpdateOptions,ReplaceOptions,DeleteOptions,CountOptions,FindOneAndUpdateOptions[A],FindOneAndReplaceOptions[A],FindOneAndDeleteOptions[A].deleteOne,deleteManyandcountgained an options parameter;WriteCommandandRepositoryfollow. Projectionsplit into inclusion and exclusion types — mixing them no longer compiles.IndexandWireCodecConfigarefinal classes withwithXbuilders instead ofcase classes.Field.hasTypetakesBsonTypeName;BsonError.TypeMismatchcarries it in both fields.MongoCollectionandMongoDatabasegained three abstract methods;Stagegained two cases.FakeMongoCollectionmoved tomongo4s.testkit.
Fixed
- A sealed hierarchy stored as the root entity of a direct collection could be written but never read back: the
decoder demanded
_typefirst, and the server stores_idfirst.EitherandIorhad the same defect. - Every write threw
UnsupportedOperationExceptionunder an unacknowledged write concern (w=0). Decimal128lost precision above 2^53; theLongrange check admitted 2^63; a non-finite value threw past theEither.- An explicit
BsonEncoder/BsonDecoderpair was ambiguous with automaticWireCodecderivation, so a type carrying both resolved no codec. cats.data.Iordecoded its two sides by position, swapping them when the stored order differed.SortandIndexkept a repeated field's original position.insertManyreturned ids in the driver's map iteration order rather than by command position.aggregate(...).firstappended$limitafter$out/$merge, which the server rejects.- A mixed projection silently returned extra fields;
excludeIdfollowed byincludelost the_idexclusion. WatchOptions' three starting points were not mutually exclusive.- A bare discriminator for a case with fields raised
IndexOutOfBoundsException; 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. - kyo's
streamfailed as aClassCastExceptionwhen handed aStreamableit had not derived. - Documentation: a broken repository example, the
Effectmember list, the moduleWithIdbelongs to, thewatchAsAttemptingnaming, and the claim that nothing but the imports changes when swapping runtimes.