SENDLINE open source
open source / mongo4s / changelog

Changelog

← back to the README

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 still mongo4s.
  • org.mongo4s 6.0.0 is a relocation to now.sendline for 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, and ReadmeReferencesSpec checks 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-reactivestreams 5.6.0 to 5.13.0 nest 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 the StackOverflowError never reaches the caller, so the find waits forever (JAVA-6313). A link that hands the client a few kilobytes at a time is enough — TLS, or Docker Desktop's host.docker.internal, where a 3.5 MB reply took over 1,500 reads. The driver is 5.14.0 now, which runs those reads in a loop. LargeReplyItSpec reads a 3 MB batch through a proxy that passes the server's answer on a kilobyte at a time; on 5.13.0 it times out with the overflow in the log.
  • On kyo, guaranteeCase reported a cancellation as an error. kyo 1.0.0-RC7 does not resume a fiber interrupted while it waits — on the server, in a sleep, in Async.never — but abandons it, and closes what the fiber owed with KyoException("fiber abandoned") rather than the interrupt. The finalizer saw ExitCase.Errored of that exception instead of ExitCase.Canceled, and so did what bracketCase acquired. A transaction still rolled back, since withTransaction aborts on either, but code on Effect[KIO] that tells the two apart — metrics, logging — counted every timeout as a failure. guaranteeCase now records how the body ended, and a scope that closes with nothing recorded is a cancellation, whatever kyo reports. cats, zio and kyo each have a spec that interrupts a body inside guaranteeCase now; none did.

Changed

  • mongodb-driver-reactivestreams 5.14.0.

5.1.0

Binary compatible with 5.0.0, checked by MiMa.

Added

  • A ScalarWireCodec is the BsonEncoder and BsonDecoder of its type. Filters, updates, keys, selectAs and distinct find it in the type's companion or an import, so given BsonEncoder[X] = codec.toBsonEncoder is no longer needed; one written by hand still wins. ScalarWireCodec.toBsonDecoder, and toBsonCodec on every WireCodec, document-shaped ones included.
  • BaseMongoRepository.direct builds a repository over getDirectCollection and answers it as a Repository[F, S, E, K], so the type it is assigned to fixes E and K — no type arguments, no .widen — and the codec's naming applies without a FieldNaming argument. A projection argument does what withoutId does.
  • PrimaryKey.of keys 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, ensureKeyIndex included. KeyRef.keyPaths is 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.
  • @stored and @notStored on 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 or None, with no codec needed for its type. Field.of, PrimaryKey.of and selectAs spell a @stored field the same way, and refuse a @notStored one. A field with no codec is now reported by name when its class derives.
  • Sums shaped for collections someone else defined. WireCodecConfig.withDiscriminatorField names the field that holds the case (_type by default); withWrappedCases writes each case as one field named after it, {"Circle": {"radius": 2.0}}; withFallbackCase reads a case it does not know as a case object of that name, for rolling deployments.
  • Named scalar representations: ScalarWireCodec.instantAsEpochMillis, instantAsEpochSeconds, instantAsIsoString, uuidAsBinary, and for LocalDate, which has no default, localDateAsString and localDateAsDate(zone) — each ready to be made the given where a collection already stores the type that way.
  • A UUID stored 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, and WireCodec.decodeOnly(decoder), for a type that is only read.
  • Map keyed by more than String. A KeyCodec[K] writes each key as the field name it has to be — Int, Long and UUID have one, and any type whose ScalarWireCodec writes a string gets one — so Map[Provider, V] derives without a hand-written codec, and field.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) and upsertKeeping (an upsert that leaves the fields another writer owns as stored), and findOneAndReplace(entity) and findOneAndDelete(key). Each has a body on Repository, so a hand-written implementation keeps compiling.
  • FakeRepository.direct and FakeMongoCollection.direct build a fake from a WireCodec, its naming included, and FakeRepository is open, so a service's own repository trait can mix into it.
  • MongoClient.ping(timeout), false when the server does not answer in time, and in mongo4s-cats MongoClientResource.reachability(client, interval), a background ping whose last answer a readiness check reads.
  • BaseMongoRepository.objectIdDirect, and a WireCodec for WithId[Id, E] whenever E is a derived case class: _id first, the entity's fields beside it, under the entity's naming.
  • MongoError.writeConcernErrors, with MongoError.WriteConcernFailure: what the write concern reported, on a single write, a bulk write and a client bulk write.
  • field.setNull and Update.setNull(field) on an Option field: the explicit null that set(None) no longer writes.
  • Typed paths through an Option and into a collection's elements. field / inner takes the inner case class's own Field when field holds it in an Option, a collection, or an optional collection, and spells every segment through the naming — itemsField / skuField is items.sku, where itemsField / "sku" stays a stored segment the naming never renames. The array operators also apply to an Option of a collection.
  • Joins that take the foreign collection. Stage.lookup, lookupWith, lookupWithKeys and graphLookup accept the foreign MongoCollection in place of its name, and render its fields, sub-pipeline and restrictSearchWithMatch through that collection's FieldNaming rather than the one running the aggregation, so collections with different namings join on the right fields. The string forms are unchanged.
  • Float, Short, Byte, Char and Array[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) on MongoDatabase and MongoCollection, the driver's client-side operation timeout for one handle rather than the whole client; an operation that runs out fails as MongoError.ExecutionTimeout. A MongoCollection or MongoDatabase of your own inherits a default that refuses it by name.
  • A PrimaryKey for WithId[Id, E] over any id with a BsonEncoder, keyed on _id, beside the one for ObjectId that was there.
  • $text with its language and sensitivities: Filter.text(search, TextSearchOptions.default.withCaseSensitive(true)), with withLanguage and withDiacriticSensitive beside it. The filter is a new Filter.TextSearch case; the Filter.text(search, language) that exists builds the Text case it always did.

Changed

  • field.set(None) renders $unset, not $set: null, so a document updated to None has the shape the default codec gives one inserted with it — $exists, sparse indexes and $type no longer depend on which write touched it last. setOnInsert(None) adds nothing to the update, and Repository.updateField(key, field, None) removes the field. Where the null was wanted, setNull writes it. Documents already carrying a null read back as None as 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 _type is 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: snakeCase and kebabCase rename without boxing a Char per 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 ScalarWireCodec encodes a query value without a writer of its own. toBsonEncoder — the BsonEncoder a 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 $in over a thousand opaque ids renders 2.1× as fast and allocates what one over plain strings does (BENCHMARKS.md).
  • A WireCodec decoding failure names where it happened. Each field, array position, map key, sum value and Ior side wraps the failure in BsonError.Nested, so the message reads address -> city -> Expected string but got int instead of the leaf alone; code matching a field's TypeMismatch directly now finds it inside a Nested. A reader exception for a value of the wrong shape is reported the same way, as a DecodingFailure, rather than as the driver's BsonInvalidOperationException.
  • A BsonDecoder collection names the element that failed. List, Vector, Seq and Set stop at the first element that does not decode and report it under its position, Map under its key — they reported the last failure, and a list's without saying where. Vector, Seq and Set no longer build a List decoder for every value they read, nor a List on the way to their own collection.

Fixed

  • The website told users to avoid getDirectCollection for a collection with mixed numeric widths, a limit 3.0.0 had already removed, and said aggregate never reads directly although aggregateDirect does. Both are corrected, along with its benchmark figures and counts, the claim that kyo and rapid read the runtime's clock for monotonic (they keep System.nanoTime), the README's MongoCollection sketch, the bson-cats-data example (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/$max update or a group's $min/$max, throwing cannot compare where the server answers — and every codec but the direct one writes None as null. 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.
  • imap or iemap on a derived case class moved its fields under value when that class was a case of a sealed trait or a branch of an Either — 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 through WireCodec.from(codec, codec), which keeps it.
  • A field whose codec reads null as a value was reported missing. Derived products told an unread field from a read one by a null in the decoded slot; they use a marker of their own now.
  • A _type that is not a string was the reader's own exception. It is a TypeMismatch under _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-inlines in 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.
  • MongoError misfiled the failures operations teams alert on. An operation whose client timeoutMS ran out was Unavailable, because the driver's MongoOperationTimeoutException extends the server-selection timeout; it is ExecutionTimeout now, and withTransaction no longer re-commits a commit that ran out of it. Refused credentials (MongoSecurityException) were Failed and are Unauthorized. A primary that stepped down, a node recovering, a cleared connection pool and a closed server were Failed and are Unavailable.
  • A bulk write that failed only on its write concern raised a BulkWriteFailed with nothing in it. failures is still empty — no document failed — and writeConcernErrors now says what did.
  • A filter that matches nothing was planned as a collection scan. in(Nil), containsAll(Nil), Filter.none and an empty KeyRef.inFilter rendered {$nor: [{}]}, which the server reads every document to answer. They render {_id: {$in: []}} now, answered from the _id index. Filter.Or(Nil), which rendered an $or the server refuses, and !Filter.all render the same way, and !Filter.none renders {}. 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. where takes only an array filter identifier now, as Field.element builds it, and refuses anything else when the path is built.
  • A date's plus and minus took only a Long amount, so Expr.lit(1) or an Int field did not compile; the server takes either, and so do they.
  • Some refusals were thrown where the operation was built, not raised in F, so attempt and handleErrorWith never saw them: an aggregation or a distinct whose filter does not render (an unclosed polygon ring), create options that contradict each other, a findPage of no entities, a Counters.next step that does not move forward, and the driver's own argument checks on listIndexes, dropIndex, dropIndexes, drop, runCommand, dropCollection and 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.sum and avg over a field took any field, and the server answers 0 for a sum of strings. They compile only on a numeric field or an Option of one now, as median and the standard deviations already did; code compiled against 5.0 keeps linking, since the unconstrained methods are still in the bytecode.
  • On zio, attempt and handleErrorWith recovered a defect as an error, so a ZIO.die in a transaction body left withTransaction as a typed failure. Only failures are recovered now; a defect still rolls the transaction back and then stays a defect. A function given to map or flatMap that throws, and a suspend whose 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 as withSession or client.withTransaction had started its session skipped the release, and one landing after a body succeeded could skip a guaranteeCase finalizer. bracketCase installs 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.withTransaction inside another is a transaction of its own, which the outer rollback leaves alone, and a session.withTransaction on 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 called 0; $size, $type, $mod, $elemMatch and the $bits operators did not look through an array of documents, and $type, $mod and $bits not into an array of scalars; a path went on into an array nested in an array, which the server does not; $all did not match a scalar field; a regular expression inside $in never 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/$mul of a non-number, failed the same way rather than with the server's codes 28 and 14; a negative $position prepended instead of counting from the end; $pull compared a document for equality instead of reading it as a query on each element; $pull, $pullAll and $addToSet told 1 from 1L; an insert stored a generated _id last; 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 distinct over a dotted path through an array dropped the field; distinct and $group told 1, 1L and 1.0 apart; first decoded every match, so a later document the codec could not read failed it; a bulk write error other than a duplicate escaped as a bare MongoError instead of a BulkWriteFailed; and explain and watch threw where they were called rather than in the effect or the stream. Each is checked against MongoDB 8.2 now.
  • The README said an Either with 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.
  • findMany returned a document once per batch its key appeared in. A key repeated further down the list than batchSize went 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. A derives WireCodec on a case class with a default parameter, and PrimaryKey.compound, warned at the user's call site on 3.10, which a build with -Werror refused. Both build their pairs as tuples now.

Changed

  • The integration tests compile on Scala 3.10 against the library built by 3.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/map over a typed element), date and conversion operators, each compiling only where the server would accept it. $addFields takes computed fields ("total" := price.expr * qty.expr, or a Field target spelled through the naming), and so do $group keys (groupByExpr, groupByKeys), accumulators (Accumulator.sum(expr) and the rest), $replaceWith, $redact, an inclusion Projection (compute, in find as well as in $project), Filter.expr and pipeline updates.
  • Atlas Search. $search and $searchMeta over typed SearchOperators — text, phrase, autocomplete, wildcard, regex, equalTo, ranges, exists, compound — and $vectorSearch, approximate or exact, with a Filter pre-filter; createSearchIndex(es), updateSearchIndex, dropSearchIndex and listSearchIndexes, with SearchIndex.dynamic, SearchIndex.of and typed vector indexes. Score with Expr.searchScore. Checked against a mongodb-atlas-local container.
  • The client-level bulkWrite (MongoDB 8.0+): each collection's write(command) addresses a WriteCommand to its namespace with its own codec, and client.bulkWrite sends writes to several collections and databases in one command, failing as MongoError.ClientBulkWriteFailed.
  • Typed positional updates. field.each ($[]), field.matched ($) and field.where(element) ($[id]), with Field.element building both the path and its array filter, and / with a Field reaching into the element through the naming.
  • pullWhere removes array elements by a condition, and Field.self states a condition on a scalar element itself — for pullWhere and for a range inside $elemMatch.
  • Insert options: InsertOneOptions and InsertManyOptions with comment and bypassDocumentValidation, and InsertManyOptions.unordered, which carries on past a duplicate. The repository's insertMany takes them too.
  • let variables on every write, on find and on aggregate; sort on updateOne/replaceOne (8.0+); comment on count; options on the bulk delete commands.
  • Cursor options: cursorType (a tailable cursor streams as a live stream), maxAwaitTime, noCursorTimeout and allowDiskUse on find; maxAwaitTime and bypassDocumentValidation on aggregate; hint and comment on distinct; EstimatedCountOptions.
  • $type over several types (hasAnyType), $currentDate as a timestamp (Update.currentTimestamp), $unwind's includeArrayIndex, $unionWith with a pipeline, and a text index's weights, default language and language override.
  • insertIfAbsent on 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) gives next, current and reset over one document per counter, next being one atomic findOneAndUpdate; FakeCounters in the testkit.
  • WireCodecConfig.withDecodeMissingAsDefault fills 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 sort on single-document updates, pullWhere, conditions on scalar elements, $type over several types and a timestamp $currentDate.
  • The fake simulates update-based upserts — updateOne, updateMany, findOneAndUpdate, findOneAndReplace and the bulkWrite update 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 _id from the filter.

Changed

  • insertOne/insertMany take options, and bulkWrite takes BulkWriteOptions instead of ordered — bulkWrite(commands, BulkWriteOptions.unordered) — on the collection and on the repository.
  • Stage.addFields takes computed fields — "name" := expr — rather than (String, BsonValue) pairs.
  • AST cases changed shape: Stage.Group takes a GroupKey, Filter.Expr and Stage.Redact an Expr, Filter.Type a list of types, Update.CurrentDate whether it writes a timestamp, Projection.Include its computed fields, Stage.Unwind includeArrayIndex, Stage.UnionWith a pipeline, and WriteCommand.DeleteOne/DeleteMany their DeleteOptions.
  • FindQuery, AggregateQuery, DistinctQuery, MongoCollection, MongoClient and Repository have new abstract members.
  • containsAll(Nil) matches nothing, as $all: [] does on the server — it matched everything before.
  • A sort on updateMany is refused, where the driver drops it without a word.
  • The build fails on any compiler warning.
  • zio 2.1.27, Testcontainers 2.0.5, sbt 2.0.11, MiMa 1.2.1.

Fixed

  • ensureKeyIndex failed on any repository keyed on _id — objectId, WithId, storedId — since the server refuses a unique flag on the _id index. 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.raw wrote window into the document it was given, so reusing it in a $group failed on the server.
  • A transaction whose commit failed carried a misleading suppressed IllegalStateException from 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 a unique index on _id and 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/bottom and topN/bottomN rank by a Sort and output a field; firstN/lastN, maxN/minN; median and percentile, always with the server's approximate method; 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 to setWindowFields beside 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, through FillOutput. The two sort-driven methods are refused without a sortBy.
  • $redact, with Stage.Descend/Prune/Keep for the expression's answers.
  • Stage.lookupWithKeys — the concise $lookup that joins on localField/foreignField and narrows with a sub-pipeline. The keys are typed on both sides.
  • $bit (bitAnd/bitOr/bitXor) and the $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear filters, on integer fields only through BitwiseOf; Filter.jsonSchema.
  • The fake simulates the new accumulators except median/percentile, and $fill, $bit and the $bits filters — each checked against MongoDB 8.2 by a parity spec.

Changed

  • setWindowFields takes WindowFields, the common parent of WindowOutput and WindowFunction; every existing call compiles unchanged.
  • mongodb-driver-reactivestreams 5.13.0; kyo 1.0.0-RC7.
  • An .editorconfig trims trailing whitespace, which is what turned the format check red.
  • The benchmarks are re-measured against mongo4cats 0.8.0 (and circe 0.14.17, whose decoders now answer an Either), 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 between mongo4s and mongo4cats are now visible. mongo4cats' find(...).stream on cats is ~4× faster in 0.8.0 than in 0.7.18, measured side by side, and still about half of mongo4s'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 _id or 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, $replaceRoot or a Stage.raw — and is taken by updateOne, updateMany, findOneAndUpdate and a bulk UpdateOne/UpdateMany. Update.Raw could not carry one, since a pipeline is a list of stages rather than a document.
  • $geoNear, $unset, $sortByCount and $bucketAuto stages. Stage.geoNear sorts by distance and writes it into a field you name, with GeoNearOptions for a query, distance bounds, a multiplier and includeLocs. Stage.unset drops fields, one and then more with and. bucketAutoRounded rounds $bucketAuto's boundaries to a BucketGranularity series, and compiles only on a numeric field, the only kind the server rounds.
  • createIndexes, dropIndexes and renameCollection on MongoCollection. createIndexes sends one command and returns the names in the order given; renameCollection refuses to replace an existing collection unless dropTarget is set.
  • MongoClient.withSession and SessionOptions. 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. startSession takes the same options.
  • The findOneAnd* options carry collation, hint, maxTime and comment, and the update and replace ones bypassDocumentValidation, which the plain writes already had.
  • ChangeEvent.namespace and destination — 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, a TTL of zero asked for on purpose: each document expires at the time its indexed field holds.
  • BaseMongoRepository.createDirect, the getDirectCollection counterpart of create.
  • findPage walks backwards with descending = 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 Update that 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.
  • findPage returns a KeysetPage(items, next). next is the key the following page starts after, or None on the last page; the repository reads one row past the page to know which.
  • watchAttempting yields ChangeEvent[DecodeResult[A]] instead of DecodeResult[ChangeEvent[A]], so an event whose document does not decode still arrives, with its resume token, and a consumer can checkpoint past it.
  • distinct over 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, hasSize and popFirst/popLast compile only on the fields they apply to — text, numbers, collections — rather than rendering something the server accepts and that does nothing.
  • A limit or batchSize below one and a negative skip are refused where they are set. MongoDB reads a limit of 0 as no limit at all.
  • FakeMongoCollection and FakeRepository stream through the runtime's RsBridge instead of taking an emit function. Streams read the collection when they run, and aggregate, distinct and selectAs stream too.
  • MongoCollection has three new abstract members and Stage four new cases, so an implementation of your own and an exhaustive match over stages have more to cover.
  • The fake's listIndexes describes each index the way the server does — v, key, name and its options, after the _id_ index — rather than listing bare key documents.
  • A chain of && or || renders as one flat $and/$or.
  • WireCodec.spellsAs is removed; nothing had asked it since getDirectCollection stopped taking a naming.

Fixed

  • A sum type's codec reported identity naming whatever it wrote, and a codec mapped with imap/iemap dropped the naming of the one underneath. On a direct collection the codec decides how queries spell fields, so a snake_case enum entity, or an entity codec wrapped in iemap to validate what it reads, got camelCase filters and updates that matched nothing.
  • emap/iemap over an Option codec 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, insertMany and upsertMany.
  • A transaction commit that ran out of maxCommitTime was 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 when use threw the same way.
  • Cancelling or timing out a unit call left its subscription running on cats-effect and kyo.
  • Repository.findOneAndUpdate replaced the projection you passed with its defaultProjection.
  • zio and rapid asked a cursor for a whole buffer to answer one or option; they ask for the two rows those read, as cats-effect and kyo do.
  • A maxTime, maxCommitTime or maxAwaitTime under a millisecond truncated to 0 — for maxTime, no limit at all. It is refused where it is set.
  • Either's codec let a branch with a field named _type overwrite 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-finite Double instead of returning a Left.
  • The document codec bridge caught fatal errors and interruptions along with decoding failures.
  • selectAs rendered its field names and looked up their decoders again for every field of every document.
  • Repository.exists counted every match; it stops at the first.
  • FakeMongoCollection: a duplicate within one insertMany was stored twice, an unordered bulkWrite stopped at its first duplicate, and either reported a DuplicateKey where the server reports a BulkWriteFailed naming the command; a unique index was recorded and not enforced, and named differently from the server's, so dropIndex missed it; a stream was fixed when it was built; a findOneAnd* that could not decode its document answered None; a collation was ignored; numbers compared through Double, so large Longs tied and 5L did not equal 5; a replacement dropped the _id it 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; and dropIndex of 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, updateByField and deleteByField, completing the grid the renames above opened up: every condition-based operation now has both a field-and-value and a whole-Filter spelling. Each is its …ByFilter counterpart over field.equalTo(value).
  • WireCodec no longer derives itself for any case class it meets. It comes from derives WireCodec, from a given you wrote, or from an existing BsonEncoder/BsonDecoder pair. A field whose type has no codec is a compile error naming the type, rather than a silently derived codec — and an opaque type keeps the representation you give it. The cases of an enum and the leaves of a sealed trait still derive with their parent, because the language gives them nowhere to write derives.
  • Filter, Update, Stage and Accumulator expose key, the MongoDB operator each case renders as, declared on the case instead of repeated in toBson.
  • Sort.byTextScore — {"$meta": "textScore"}, so a $text search can be ranked by relevance. Sort now carries a SortOrder per field instead of a Boolean.
  • Projection.slice and Projection.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.text and Index.geo2D on 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.
  • explain on FindQuery and AggregateQuery, taking an optional ExplainVerbosity and returning the server's plan as a BsonDocument. It explains the query the builder already produced, and the whole pipeline rather than the $limit-ed form first sends. The fake refuses it by name.
  • MongoError, a typed failure for everything the server reports: DuplicateKey, WriteConflict, ExecutionTimeout, Unauthorized, Unavailable, BulkWriteFailed and a Failed catch-all that keeps the server's code. Translation happens on the Publisher, 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 as cause.
  • FakeMongoCollection.distinct is simulated, array fields flattened into their elements as the server does, and checked against a real server by a parity spec.
  • FakeMongoCollection.aggregate now simulates $match, $sort, $skip, $limit, $project, $count and $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, $addToSet included — MongoDB leaves the order of its result undefined. A parity spec checks the subset against a real server, BSON types and all.
  • ScalarWireCodecBenchmark, AggregateBenchmark and ErrorTranslationBenchmark, and the numbers they produced, in BENCHMARKS.md: what a bridged scalar costs, what aggregateDirect is worth against a real server at ten documents and at ten thousand, and what the MongoError wrapper costs on a publisher that never fails — nothing measurable, at one element and at ten thousand alike.
  • Native WireCodec instances for BigDecimal, Instant, UUID and ObjectId. Before this, the AST-free path had them only through the BsonEncoder/BsonDecoder bridge — one BsonValue per 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's readConcern, writeConcern, readPreference and maxCommitTime — and the retry window below. Both withTransaction methods 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. Unlike skip it 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 a WireCodec[B] rather than a BsonDocumentCodec[B]. On a direct collection that is AST-free, the same trip find makes 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's WireCodec demands every modelled field.
  • Geospatial filters: near, nearSphere, within and intersects on a Field, over a GeoJSON Geometry vocabulary (Point, LineString, Polygon, and the Multi* forms) plus the legacy 2d shapes in GeoShape. A polygon ring that does not close is refused rather than sent.
  • Stage.bucketBy, Stage.densify and Stage.setWindowFields — $bucket, $densify and $setWindowFields, with DensifyRange/DensifyBounds/DateUnit and Window/WindowBound/WindowOutput for the vocabulary they need. $bucket's boundaries are values of the field being grouped rather than raw BSON.
  • CreateCollectionOptions, taken by MongoDatabase.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 without capped, a validation level without a validator — are rejected rather than sent.
  • WatchOptions.withExpandedEvents — the driver's showExpandedEvents, so DDL events arrive alongside document ones. Needs MongoDB 6.0, and the flag is sent only when asked for.
  • ChangeEvent.wallTime and ChangeEvent.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 reading System.nanoTime, so implementors are unaffected; mongo4s-cats and mongo4s-zio override it with their runtime's own clock.

Changed

  • Repository's condition-based methods say what they take. findBy is findByField, updateBy is updateByFilter, deleteBy is deleteByFilter and getBy is getByFilter. By used to mean a field in findBy and a filter in updateBy/deleteBy/getBy, while the filter-taking read was findByFilter — so the same idea had two names and one name had two meanings.
  • Repository.insertOne returns K and insertMany returns List[K], instead of the driver's Option[org.bson.BsonValue] and List[org.bson.BsonValue]. A repository is parameterised on its key type and can name the key itself through PrimaryKey, 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 returns InsertOneResult/InsertManyResult exactly 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 the BsonValue should drop the decode.
  • The direct path reads numbers the way the BsonValue path does. A Long field stored as an Int32, a Double stored as a whole Int64, and every other lossless width now decode through getDirectCollection as they always did through getCollection; what would lose information is still refused on both. The rule lives in BsonDecoder alone and the wire codecs defer to it. A wrong type is now a BsonError.TypeMismatch rather than a raw org.bson.BsonInvalidOperationException, and BsonError.fromThrowable unwraps a DecodingFailure instead of re-wrapping it as Thrown.
  • Driver exceptions no longer reach your code from an operation. com.mongodb.MongoWriteException and friends arrive as MongoError instead; code that caught the driver's types directly has to match on MongoError, or on cause, which still holds the original. Errors mongo4s raises itself — BsonError.DecodingFailure, RsBridgeError — are unchanged.
  • RsBridgeConfig is a final class with a private constructor and withX builders, and RsBridgeConfig.Default is now RsBridgeConfig.default — the same shape every other options type already had, and the last one that could not gain a field without a major. bufferSize must now be positive.
  • JDK 25 is now required to build, and the build says so before compiling anything instead of failing on the fifth module with UnsupportedClassVersionError: class file version 69.0. mongo4s-kyo sets the floor — kyo's Frame macro 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 that mongo4s-kyo, like kyo itself, needs Java 25 at runtime.
  • Scala 3.9 LTS is now required, and it is the only Scala version the build uses. mongo4s-bson-calypso, mongo4s-kyo and mongo4s-rapid were pinned to a fast-release 3.8 because their dependencies needed a compiler newer than 3.3 LTS; they are now on the LTS line with every other module. It takes a major version because TASTy does not read forward, so a 3.3 LTS project cannot consume these artifacts, and MiMa cannot see that break. 3.3 LTS is no longer supported.
  • withTransaction retries the way the driver's own does: a TransientTransactionError restarts the whole transaction, an UnknownTransactionCommitResult retries only the commit, both bounded by one 120-second deadline taken before the first attempt. 2.x reported the first failure; TransactionOptions.withoutRetries restores 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/_._2 extractors. Any width works, so compound3 and compound4 were removed.
  • MongoDatabase.createCollection takes a CreateCollectionOptions, defaulted, so the no-options call is unchanged at the source level.
  • WatchOptions is a final class with withX builders instead of a case class — same reason Index and WireCodecConfig already were. Build from WatchOptions.default[E]; reading its fields is unchanged.
  • The accessors the compiler synthesizes for Field.of and the two WireCodec derivations are pinned with @publicInBinary. They were binary-unstable — a name MiMa cannot check because it is synthesized, not declared.
  • mongo4s-cats builds against fs2 3.14.0. Its Reactive-Streams interop is unchanged from 3.13.0 — the module is identical between the two releases — so nothing about streaming behaviour moves with the bump.
  • Internally, every given moved to the syntax SIP-64 introduced in 3.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

  • FakeMongoCollection reported an upsert that inserted as if it had done nothing. replaceOne with upsert = true stored the replacement without stamping an _id, so upsertedId came back None and wasUpserted/wasApplied came back false where the server returns the new id and true — a test asserting on wasUpserted passed against the fake and inverted against a real server. The bulk path was worse: it reported the entity's own id field, or the whole document as JSON when there was none, instead of the _id. Both now go through the same _id stamping and duplicate check insertOne uses, as does a bulkWrite insert, which was skipping it too. A parity spec pins all four against MongoDB 8.2.
  • Streamable.instance is private[mongo4s]. It satisfied any Streamable[S, A], so a hand-written given for 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.traverse is Effect.flatTraverse: its function returns F[List[B]] and the result is flattened, which is not what traverse means anywhere else.
  • mongo4s.bson.DecodeResult moved from mongo4s-core to mongo4s-bson-core, so the mongo4s.bson package lives in one artifact instead of being split across two.
  • $min and $max were gated on NumericOf, so lastSeen.max(now) on an Instant field did not compile although MongoDB orders dates perfectly well. They take a new ComparableOf — numbers, String, Instant, ObjectId, UUID, Boolean, BigDecimal, and their Options. $inc and $mul remain numeric.
  • aggregate[B] asked for a BsonDocumentCodec[B] while only ever decoding, so a read model had to carry an encoder that nothing called. It takes a BsonDocumentDecoder[B] now, like watchAs beside it.
  • A snake_case (or otherwise renamed) WireCodec and the queries against its collection could disagree about field names, and every query then matched nothing. getDirectCollection no longer takes a naming at all: a derived codec reports the WireCodecConfig it was built with, a hand-written one declares its own by overriding fieldNaming, and the queries follow whichever it is.
  • selectAs reported MissingField for an Option label whose field the document did not carry, while reading the whole entity gave None for 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.cancel cancelled the subscription but left no terminal in the queue, so a consumer already parked in hasNext stayed parked — a leaked thread per cancelled or timed-out rapid stream. It now enqueues the end of the stream, and a spec asserts the parked consumer is released.
  • ReadmeReferencesSpec reads README.md and fails if a Scala block names an Object.member the source does not define. The compiled snippets under examples/ are re-typed copies, so they could be right while the README was wrong — which is exactly how Index.geo2dsphere survived beside the geo2DSphere that exists.
  • Repository.bulkWrite had no ordered flag, 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 takes ordered now, batches only when ordered (where batching preserves the meaning), and sends an unordered bulk as one command.
  • FakeMongoCollection simulated four update operators out of eighteen while the documentation said updates were simulated, so a unit test reaching for push, pull, pop, rename, mul, min, max, addToSet or currentDate failed with UnsupportedOperationException. All of them are simulated now, $push's $each/$position/$slice/$sort modifiers included, and FakeUpdateParityItSpec checks each against a real server.
  • FakeMongoCollection answered several queries differently from the server. A filter over an array field compared the whole array to the value, so contains never matched and $ne always did; a dotted path through an array of documents resolved to nothing; $regex anchored the whole string and dropped its options; $inc truncated its amount to a Long and rewrote the field as Int64, after which the fake could not find the row it had just updated; $count reported 0 where the server reports nothing; findOneAndUpdate/Replace/Delete ignored their sort; $eq/$ne against null disagreed with the server about a missing field; sorting or comparing a date, ObjectId or boolean threw; $first/$last skipped documents missing the field instead of taking the first or last; a bulk UpdateOne/UpdateMany silently discarded its UpdateOptions; inserts neither stamped an _id nor refused a duplicate one. Every one of these is now checked against a real MongoDB by FakeFidelityParityItSpec.
  • RsBridge.unit reported a publisher's failure wrapped in a CompletionException on 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 carrying FakeMongoCollection — previously unpublished test sources — and a new FakeRepository, a real BaseMongoRepository over a fake collection.
  • arrayFilters on the update operations, with $[] and $[identifier] built as ordinary path segments.
  • collation, hint and comment on every write; bypassDocumentValidation where a whole document is written; limit, skip and maxTime on count.
  • $each modifiers on pushAll: $slice, $sort, $position.
  • Index types hashed, 2dsphere and 2d, plus hidden, collation and wildcardProjection.
  • $out and $merge take a target database, and $merge its on/whenMatched/whenNotMatched policies.
  • $lookup in its sub-pipeline form with let, and $graphLookup.
  • WireCodecConfig.omitNoneFields (default on): a None field is left out rather than written as null.
  • BsonTypeName is 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, deleteMany and count gained an options parameter; WriteCommand and Repository follow.
  • Projection split into inclusion and exclusion types — mixing them no longer compiles.
  • Index and WireCodecConfig are final classes with withX builders instead of case classes.
  • Field.hasType takes BsonTypeName; BsonError.TypeMismatch carries it in both fields.
  • MongoCollection and MongoDatabase gained three abstract methods; Stage gained two cases.
  • FakeMongoCollection moved to mongo4s.testkit.

Fixed

  • A sealed hierarchy stored as the root entity of a direct collection could be written but never read back: the decoder demanded _type first, and the server stores _id first. Either and Ior had the same defect.
  • Every write threw UnsupportedOperationException under an unacknowledged write concern (w=0).
  • Decimal128 lost precision above 2^53; the Long range check admitted 2^63; a non-finite value threw past the Either.
  • An explicit BsonEncoder/BsonDecoder pair was ambiguous with automatic WireCodec derivation, so a type carrying both resolved no codec.
  • cats.data.Ior decoded its two sides by position, swapping them when the stored order differed.
  • Sort and Index kept a repeated field's original position.
  • insertMany returned ids in the driver's map iteration order rather than by command position.
  • aggregate(...).first appended $limit after $out/$merge, which the server rejects.
  • A mixed projection silently returned extra fields; excludeId followed by include lost the _id exclusion.
  • WatchOptions' three starting points were not mutually exclusive.
  • A bare discriminator for a case with fields raised IndexOutOfBoundsException; a subtype declaring its own _type field wrote duplicate BSON keys.
  • rapid's MongoClientResource leaked the client when use threw while its task was being built.
  • zio's bracketCase swallowed a finalizer error on success.
  • kyo's stream failed as a ClassCastException when handed a Streamable it had not derived.
  • Documentation: a broken repository example, the Effect member list, the module WithId belongs to, the watchAsAttempting naming, and the claim that nothing but the imports changes when swapping runtimes.
built from CHANGELOG.md at v6.0.0 edit this page on GitHub