Skip to content

Releases

v6.0.0

Choose a tag to compare

@shadowsmind shadowsmind released this 07 Nov 12:35

mongo4s 6.0.0

mongo4s moves to the now.sendline organization. The code and API are the same as in 5.1.1, and the package is
still mongo4s. MiMa checks every module against org.mongo4s 5.0.0 and 5.1.1 and finds nothing. The upgrade is
one line in build.sbt.

libraryDependencies += "now.sendline" %% "mongo4s-core" % "6.0.0"

Moving from 5.1.x

// before
"org.mongo4s" %% "mongo4s-cats" % "5.1.1"
// after
"now.sendline" %% "mongo4s-cats" % "6.0.0"

A build tool treats a new organization as a new library and will not evict one in favour of the other. If something
in your graph still brings in org.mongo4s 5.x, both jars carry the same classes, and which one loads depends on
classpath order. Move that dependency as well, or exclude the old organization:

libraryDependencies += ("com.example" %% "internal-commons" % "1.2.3").excludeAll(ExclusionRule("org.mongo4s"))

org.mongo4s 6.0.0 is published as a Maven relocation: one pom per module that points to now.sendline, with no
jar. A build that only bumps the version to 6.0.0 therefore resolves to the new organization, in Maven and in sbt
through Coursier. Maven also prints the relocation message saying where mongo4s moved. Nothing more will be
published under org.mongo4s.

Where things are now

  • Repository: Sendline-now/mongo4s. The old URLs redirect.
  • Site: oss.sendline.now/mongo4s, shared with logging4s and metrics4s and
    built from this README. mongo4s.org redirects there.
  • A new logo, in light and dark versions.

Why a major version

Nothing in the API changed. The version is major because switching the organization means editing build.sbt by
hand, and a version bump that looks routine would hide that. The reasoning is in
DESIGN.md, and the migration guide is in
COMPATIBILITY.md.

v5.1.1

Choose a tag to compare

@shadowsmind shadowsmind released this 07 Nov 09:03

mongo4s 5.1.1

Two fixes: a hang on large replies, which every runtime hits, and how kyo reports a cancellation. Binary compatible
with 5.0.0 and 5.1.0, checked by MiMa with no exclusions.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.1.1"

Bug fixes

  • A large reply read in many small pieces hung the call. MongoDB driver 5.6.0 to 5.13.0 nests one callback
    per partial socket read of a reply and unwinds them all at once when the reply is complete. About 900 reads
    overflow the stack of the driver's I/O thread. The StackOverflowError never reaches the caller, so the find
    waits forever (JAVA-6313). A link that delivers a few kilobytes per
    read is enough to trigger it: TLS does, and so does Docker Desktop's host.docker.internal, where a 3.5 MB reply
    took over 1,500 reads. mongo4s now depends on driver 5.14.0, which reads in a loop instead.
  • On kyo, guaranteeCase and bracketCase reported a cancellation as an error. kyo 1.0.0-RC7 abandons a fiber
    interrupted while it waits and closes its scope with KyoException("fiber abandoned"). The finalizer therefore saw
    ExitCase.Errored instead of ExitCase.Canceled. Transactions still rolled back, but code that tells the two cases
    apart counted every timeout as a failure. The finalizer now sees Canceled.

Dependencies

  • mongodb-driver-reactivestreams, mongodb-driver-core and bson 5.14.0. A build that pins its own org.mongodb
    jars needs 5.14.0 or later, or it still hangs.

Build

  • 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 5.14.0 it passes.
  • cats, zio and kyo each have a spec that interrupts a body inside guaranteeCase; none did before.

v5.1.0

Choose a tag to compare

@shadowsmind shadowsmind released this 02 Nov 16:11

mongo4s 5.1.0

Mostly what services moving onto the direct codec path were writing by hand: a ScalarWireCodec that answers as the
BsonEncoder of its type, a repository over a WireCodec keyed on the entity's own fields, stored names, named scalar
representations and Map keys beyond String — plus cheaper decoding and rendering, and a fake that answers more like
the server. Binary compatible with 5.0.0, checked by MiMa with no exclusions. A few things change without a compile
error; upgrade notes:
COMPATIBILITY.md.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.1.0"

New features

  • A 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. toBsonCodec on every WireCodec, document-shaped ones included.
  • BaseMongoRepository.direct builds a repository over getDirectCollection and answers it as
    F[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:
    type PlayerRepository[F[*]] = Repository[F, CatsStream[F], Player, PlayerId]
    
    def players[F[*]: Async](db: MongoDatabase[F, CatsStream[F]]): F[PlayerRepository[F]] =
      BaseMongoRepository.direct(db, "players", projection = Projection.excludeId[Player])
    objectIdDirect is the direct objectId: WithId[Id, E] has a WireCodec whenever E is a derived case class.
  • PrimaryKey.of(_.provider, _.providerId) keys an entity on one, two or three of its own fields as a plain tuple,
    each field spelled through the collection's naming wherever the key renders, ensureKeyIndex included. A
    PrimaryKey for WithId[Id, E] over any id with a BsonEncoder, beside the one for ObjectId.
  • @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 and reads it back as its default or None. Field.of,
    PrimaryKey.of and selectAs spell a @stored field the same way and refuse a @notStored one.
  • Sums shaped for collections someone else defined — WireCodecConfig.withDiscriminatorField names the field
    that holds the case, withWrappedCases writes {"Circle": {"radius": 2.0}}, and withFallbackCase reads a case
    it does not know as a case object, for rolling deployments.
  • Named scalar representations — ScalarWireCodec.instantAsEpochMillis, instantAsEpochSeconds,
    instantAsIsoString, uuidAsBinary, 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 reads on both paths.
    ScalarWireCodec.byValue(values)(_.value) for an enum stored by a value of its own, WireCodec.decodeOnly for a
    type that is only read.
  • Map keyed by more than String — a KeyCodec[K] writes each key as a field name: Int, Long and UUID
    have one, and so does any type whose ScalarWireCodec writes a string. field.at(key) reaches an entry in a query.
  • Float, Short, Byte, Char and Array[Byte] on both codec paths, stored as the Java driver stores them,
    with range checks on the way in.
  • Typed paths through an Option and into a collection's elements — itemsField / skuField is items.sku, every
    segment spelled through the naming. Field.of[E](_.field) names only the entity.
  • Joins that take the foreign collection — Stage.lookup, lookupWith, lookupWithKeys and graphLookup accept
    the foreign MongoCollection and render its side through that collection's own FieldNaming.
  • Repository methods services were writing by hand — findOneByFilter, estimatedCount, updateByKeys,
    updateEach, upsertKeeping, findOneAndReplace(entity) and findOneAndDelete(key), each with a body on
    Repository, so a hand-written implementation keeps compiling.
  • Operations — MongoClient.ping(timeout); MongoClientResource.reachability(client, interval) in mongo4s-cats,
    a background ping a readiness check reads; withTimeout(duration) on a MongoDatabase or a MongoCollection;
    MongoError.writeConcernErrors and MongoError.WriteConcernFailure.
  • Smaller additions — field.setNull; $text with TextSearchOptions (language, case and diacritic
    sensitivity); FakeRepository.direct and FakeMongoCollection.direct; FakeRepository is open, so a service's
    own repository trait can mix into it.

Behaviour changes

  • field.set(None) renders $unset, not $set: null, so a document updated to None has the shape one inserted
    with it has. setOnInsert(None) adds nothing; setNull writes the null where it is wanted.
  • A filter that matches nothing renders {_id: {$in: []}}, answered from the _id index, rather than
    {$nor: [{}]}, which the server answered with a collection scan — in(Nil), containsAll(Nil), Filter.none, an
    empty KeyRef.inFilter, Filter.Or(Nil) and !Filter.all.
  • Refusals are raised in F, not thrown where the call is built, so attempt and handleErrorWith see them.
  • On zio, a defect stays a defect. attempt and handleErrorWith recover failures only, and a throw inside
    map, flatMap or suspend fails the effect, as on the other three runtimes.
  • MongoError files failures where operations teams look for them — a client timeoutMS that ran out is
    ExecutionTimeout, refused credentials are Unauthorized, a stepped-down primary, a recovering node, a cleared
    pool and a closed server are Unavailable.
  • Decoding failures name where they happened — address -> city -> Expected string but got int — through
    BsonError.Nested; a match on a bare TypeMismatch finds it inside.
  • Accumulator.sum and avg compile only on a numeric field or an Option of one. Code compiled against 5.0
    keeps linking.
  • Filter has a new case, TextSearch, which an exhaustive match has to handle.

Performance

  • Decoding a derived entity — about 1.09× and 592 bytes less per document on a consumer-shaped entity: a
    product no longer copies its fields into a tuple on the way to its constructor.
  • Rendering a query — 1.5× on a five-condition filter and 2.4× on a ten-operator update, with half the
    garbage on the update.
  • An $in over a thousand opaque ids renders 2.1× as fast and allocates what one over plain strings does.

Bug fixes

  • On kyo, an interrupt could leave a session open. One landing just as withSession or
    client.withTransaction had started its session skipped the release. bracketCase installs its guard before it
    acquires, and every finalizer runs inside kyo's scope.
  • findMany returned a document once per batch its key appeared in. The keys are made distinct before they are
    batched.
  • where(Field.self) rendered $[], updating every element instead of the ones an array filter selects.
  • 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. Documents already written in that shape still read through
    WireCodec.from(codec, codec).
  • A sealed trait or an enum with more than 32 cases did not derive without raising -Xmax-inlines.
  • A field whose codec reads null as a value was reported missing, and a _type that is not a string failed with
    the reader's own exception rather than a TypeMismatch.
  • withTransaction re-committed a commit that ran out of timeoutMS.
  • A bulk write that failed only on its write concern raised a BulkWriteFailed with nothing in it.
  • A date's plus and minus took only a Long amount.
  • The fake failed on null, mixed types and arrays in a range filter, a sort or $min/$max, throwing
    cannot compare where the server answers; it compares in BSON's type order now. It also answers like the server on
    numeric path segments, operators through arrays, $all on a scalar, a regular expression in $in, UTF-8 string
    order, updates through array positions with the server's codes 28 and 14, a negative $position, $pull by a
    document, number widths in distinct and $group, projections through arrays, and the modifiedCount of an
    identical replacement — each pinned by a parity spec against MongoDB 8.2.
  • Docs — the website warned off getDirectCollection for mixed numeric widths, a limit 3.0.0 removed; the
    README now says that transactions do not nest, and when an Either with indistinguishable branches is refused.

Benchmarks

Re-measured on 5.1.0, every harness, against mongo4cats 0.8.0, with the machine, server and every library
version stated — BENCHMARKS.md.

  • WireCodec to real BSON bytes: 2.4–3.5× faster than either mongo4cats codec, up to 6.8× less allocation.
  • Single-document operations against a real MongoDB land level for all six stacks. On bulk reads mongo4cats
    allocates 2.1–6.1× more on findAll/findStream and reads findAll 1.12–1.36× slower; its
    find(...).stream runs at 1,471 ops/s against mongo4s's 3,109 on the same effect system.
  • A new harness, EffectBenchmark, measures a step of each runtime's Effect instance on its own: a flatMap is
    12–31 ns everywhere, a bracket 190–246 ns on cats, zio and rapid and 2.5 µs on kyo — paid once per
    session or transaction, n...
Read more

v5.0.1

Choose a tag to compare

@shadowsmind shadowsmind released this 26 Oct 20:58

mongo4s 5.0.1

A fix for users on Scala 3.10. Binary compatible with 5.0.0, checked by MiMa with no exclusions.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.0.1"

Bug fixes

  • Code inlined into the caller carried Scala 3.10's deprecated ArrowAssoc. 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 to compile. Both build their pairs as tuples now.

Build

  • The integration tests compile on Scala 3.10 against the library built by 3.9 LTS, so what the library inlines
    into its callers is checked on the compiler they are moving to. With the 5.0.0 code they fail exactly as a user's
    build did.

v5.0.0

Choose a tag to compare

@shadowsmind shadowsmind released this 26 Oct 15:17

mongo4s 5.0.0

Typed aggregation expressions, Atlas Search, the client-level bulkWrite, typed positional updates, conditional
writes and counters on the repository, and a fake that simulates upserts. A major release: several methods gained
parameters and several AST cases changed shape, so anything compiled against 4.x has to be recompiled, though most
calls compile unchanged. Migration notes:
COMPATIBILITY.md.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "5.0.0"

New features

  • Typed aggregation expressions — Expr[E, A] computes a value from a document: field.expr, Expr.lit,
    arithmetic, comparisons, logic, cond/switch, text, array (filter/map over a typed element), date and
    conversion operators, each compiling only where the server accepts it. They go into $addFields
    ("total" := price.expr * qty.expr), $group keys and accumulators, $replaceWith, $redact, computed
    projections in find and $project, Filter.expr and pipeline updates.
  • Atlas Search — $search and $searchMeta over typed SearchOperators (text, phrase, autocomplete,
    wildcard, regex, equalTo, ranges, exists, compound), $vectorSearch approximate or exact with a Filter
    pre-filter, search-index management with typed vector indexes, and Expr.searchScore. Tested against a
    mongodb-atlas-local container.
  • Client-level bulkWrite (MongoDB 8.0+) — collection.write(command) addresses a WriteCommand with that
    collection's own codec, and client.bulkWrite sends writes to several collections and databases in one command.
  • Typed positional updates — field.each ($[]), field.matched ($) and field.where(element) ($[id]),
    with Field.element building both the path and its array filter.
  • pullWhere removes array elements by a condition; Field.self states a condition on a scalar element.
  • Repository writes — insertIfAbsent(entity): F[Boolean] inserts only when the key is free, without
    overwriting and without failing a transaction; replace(entity, condition) replaces by key only while the
    condition holds, for optimistic concurrency.
  • Counters — MongoCounters.create(database, name) gives next, current and reset over one document per
    counter, next being a single atomic findOneAndUpdate; FakeCounters in the testkit.
  • WireCodecConfig.withDecodeMissingAsDefault — fill a missing field from the case-class default, for documents
    written before the field existed. Off by default.
  • Options — InsertOneOptions/InsertManyOptions (InsertManyOptions.unordered inserts past a duplicate),
    BulkWriteOptions, let variables on every write and on find/aggregate, sort on updateOne/replaceOne
    (8.0+), tailable cursors streamed live, maxAwaitTime, noCursorTimeout, allowDiskUse, hint/comment on
    distinct, EstimatedCountOptions.
  • Smaller additions — $type over several types (hasAnyType), $currentDate as a timestamp, $unwind's
    includeArrayIndex, $unionWith with a pipeline, text-index weights and languages.
  • The fake simulates update-based upserts, building the inserted document from the filter the way the server
    does, plus unordered inserts, a sort on single-document updates, pullWhere, conditions on scalar elements,
    $type over several types and a timestamp $currentDate — each pinned by a parity spec against MongoDB 8.2.

Breaking changes

  • insertOne/insertMany take options, and bulkWrite takes BulkWriteOptions instead of ordered.
  • Stage.addFields takes computed fields — "name" := expr — instead of (String, BsonValue) pairs.
  • Stage.Group, Stage.Redact, Filter.Expr, Filter.Type, Update.CurrentDate, Projection.Include,
    Stage.Unwind, Stage.UnionWith and WriteCommand.DeleteOne/DeleteMany changed shape.
  • FindQuery, AggregateQuery, DistinctQuery, MongoCollection, MongoClient and Repository have new abstract
    members.
  • containsAll(Nil) matches nothing, as $all: [] does on the server.
  • A sort on updateMany is refused rather than dropped by the driver.

Bug fixes

  • ensureKeyIndex failed on every repository keyed on _id; it now builds nothing for such a key and answers _id_.
  • Slicing a field the projection also included returned every field of the document.
  • A window over Accumulator.raw wrote into the document it was given, so reusing it in a $group failed.
  • A failed transaction commit carried a misleading suppressed IllegalStateException.
  • The fake wrote a positional path as a field called $[], accepted a unique index on _id and partial indexes the
    server refuses, matched documents on $all: [], and gave a replacement upsert a new _id instead of the
    filter's.

Compatibility

  • Scala 3.9 LTS; class files target Java 17, mongo4s-kyo needs JDK 25 at runtime for kyo's sake, and building
    requires JDK 25. Tested against MongoDB 8.2.
  • mongodb-driver-reactivestreams 5.13.0, zio 2.1.27, kyo 1.0.0-RC7. mongo4s-kyo remains outside the
    binary compatibility promise while kyo is on a release candidate.
  • The build fails on any compiler warning.

v4.1.0

Choose a tag to compare

@shadowsmind shadowsmind released this 12 Oct 22:31

mongo4s 4.1.0

The aggregation vocabulary MongoDB added in 5.x–7.0 — the ranking and statistical accumulators, window functions,
$fill, $redact and the concise $lookup — plus bitwise updates and filters. Binary compatible with 4.0.0,
checked by MiMa with no exclusions. Upgrade notes:
COMPATIBILITY.md.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "4.1.0"

New features

  • Accumulators — top/bottom and topN/bottomN rank by a Sort and output a field; firstN/lastN,
    maxN/minN; median and percentile 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.
    Putting one in a $group, where the server refuses it, does not compile.
  • $fill — by a value, by the last value seen, or linearly between neighbours, per partition, through
    FillOutput. The sort-driven methods are refused without a sortBy.
  • $redact, with Stage.Descend/Prune/Keep for the expression's answers.
  • Stage.lookupWithKeys — $lookup that joins on localField/foreignField and narrows with a sub-pipeline.
    Both keys are typed, so joining an ObjectId to a String is a compile error.
  • $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 pinned by a parity spec against MongoDB 8.2. Window functions, $redact, $jsonSchema,
    median and percentile throw UnsupportedOperationException naming what was asked for.

Bug fixes

  • The fake was not safe to share between fibers. Concurrent inserts lost documents or threw, a duplicate _id
    or unique key could get through twice, and concurrent $incs lost updates. Every operation now runs atomically,
    so a counter or a work queue claimed through findOneAndUpdate behaves under contention as it does against a
    server.

Benchmarks

Re-measured on 4.1.0 against mongo4cats 0.8.0, with the machine, server and every library version stated —
BENCHMARKS.md.

  • WireCodec to real BSON bytes: 2.4–3.2× faster than either mongo4cats codec, up to 6.4× less allocation.
  • Single-document operations against a real MongoDB land level for all six stacks.
  • Bulk reads pull ahead: mongo4cats allocates 2.1–6.0× more on findAll/findStream and reads findAll
    1.13–1.35× slower. Its find(...).stream on cats-effect runs at 1,626 ops/s against mongo4s's
    3,174 — 0.8.0 closed most of the gap 0.7.18 had (392 ops/s), not all of it.

Compatibility

  • mongodb-driver-reactivestreams 5.13.0; kyo 1.0.0-RC7. mongo4s-kyo remains outside the binary
    compatibility promise while kyo is on a release candidate.
  • Accumulator, Filter, Update and Stage have new cases, so an exhaustive match over one of them warns.
  • setWindowFields takes WindowFields, the common parent of WindowOutput and WindowFunction; every existing
    call compiles unchanged.
  • Scala 3.9 LTS; building requires JDK 25. Tested against JDK 25 and MongoDB 8.2.

v4.0.0

Choose a tag to compare

@shadowsmind shadowsmind released this 24 Sep 12:33

mongo4s 4.0.0

Pipeline updates, sessions without a transaction, $geoNear and three more stages, and a testkit fake that answers
the way the server does. Most breaks are compile errors; a few calls now refuse what they used to accept quietly.
Full migration guide: COMPATIBILITY.md.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "4.0.0"

Breaking changes

  • An Update that changes one path twice is refused when it renders — inc(n, 1).and(inc(n, 2)) used to send
    {"$inc": {"n": 2}} and lose the first change. Paths that overlap across operators are refused too; the server
    refused those anyway. No compile error behind this one.
  • findPage returns KeysetPage(items, next) — pass next as after; it is None on the last page.
  • watchAttempting yields ChangeEvent[DecodeResult[A]], so an event whose document fails to decode still
    arrives, resume token included. watchAsAttempting on the client and the database changed the same way.
  • distinct over an array answers with its elements — distinct[C, B] through DistinctOf. A call that named
    the type argument names both now, or neither.
  • regex, mod, hasSize and popFirst/popLast compile only on text, numeric and collection fields.
  • A limit or batchSize below one and a negative skip throw where they are set — FindQuery,
    CountOptions, WatchOptions, Stage and Page. MongoDB reads a limit of 0 as no limit at all.
  • ChangeEvent gained namespace and destination — positional construction takes two more arguments.
  • Update has a Pipeline case, Stage four new cases, MongoCollection three new members — an exhaustive
    match or an implementation of your own has more to cover.
  • The testkit fake takes the runtime's RsBridge instead of an emit function —
    FakeMongoCollection[IO, S, User](codec) — and its listIndexes describes indexes as the server does.
  • WireCodec.spellsAs removed.

New features

  • Pipeline updates — Update.pipeline(stages*) over $addFields, $project, $unset and $replaceRoot,
    taken by updateOne, updateMany, findOneAndUpdate and bulk updates.
  • $geoNear with GeoNearOptions — query, distance bounds, multiplier, includeLocs. $unset,
    $sortByCount and $bucketAuto; bucketAutoRounded compiles only on a numeric field, the only kind the
    server rounds.
  • createIndexes, dropIndexes, renameCollection.
  • MongoClient.withSession and SessionOptions — a causally consistent or snapshot session without a
    transaction, given to every call in the body.
  • The findOneAnd* options carry collation, hint, maxTime, comment and bypassDocumentValidation.
  • findPage(descending = true), Index.expiringAtFieldTime, BaseMongoRepository.createDirect,
    BulkWriteFailed.result, Effect.sleep.

Bug fixes

Each reproduced by a failing test first; the fake-versus-server ones are pinned by parity specs against MongoDB 8.2.

  • A sum type's codec, and one mapped with imap/iemap, reported the wrong naming — on a direct collection a
    snake_case enum entity got camelCase filters and updates that matched nothing.
  • emap/iemap over an Option codec could not read back the empty value it wrote.
  • A commit that ran out of maxCommitTime was retried until the retry window closed. Whole-transaction retries
    now back off with jitter, as the driver specification requires.
  • A transaction body that threw while being built left the transaction open, and the default bracketCase —
    kyo's and rapid's — released nothing in the same case.
  • A repository write failing in a later batch named the command by its position in that batch, and its result
    left out the batches before it.
  • Cancelling a unit call left its subscription running on cats-effect and kyo.
  • zio and rapid asked a cursor for a whole buffer to answer one or option.
  • Repository.findOneAndUpdate replaced your projection; a sub-millisecond maxTime meant no limit; Either's codec
    let a _type field overwrite the discriminator; BsonDecoder[BigDecimal] threw on a non-finite Double;
    Repository.exists counted every match; selectAs recomputed names and decoders for every document.
  • The fake answers like the server in many more places — duplicates in insertMany and unordered bulks, unique
    indexes enforced and named as the server names them, streams read when they run, collations refused, numbers
    compared across widths, replacements keep their _id, no-op updates not counted as modified, and index creation
    and drops failing where the server fails.

Benchmarks

A/B against 3.0.0 on one machine, all six harnesses: no measurable change. Allocations per operation are identical
and every difference sits inside run-to-run error; the driver bump to 5.12.0 is not slower.
BENCHMARKS.md keeps the 3.0.0 measurements, which
stand.

Compatibility

  • Scala 3.9 LTS; building requires JDK 25. mongo4s-kyo remains outside the binary compatibility promise while
    kyo is on a release candidate.
  • MongoDB Java driver 5.12.0. Tested against JDK 25 and MongoDB 8.2.
  • Not binary compatible with 3.x — see the migration guide.

v3.0.0

Choose a tag to compare

@shadowsmind shadowsmind released this 11 Sep 08:01

mongo4s 3.0.0

Whole build on Scala 3.9 LTS, typed MongoError instead of driver exceptions, and WireCodec no longer derives
itself unasked. Full migration guide:
COMPATIBILITY.md.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "3.0.0"

Breaking changes

Every item is a compile error, except the transaction retries.

  • Scala 3.9 LTS required, 3.3 LTS dropped. TASTy does not read forward, and MiMa cannot see that break —
    which is what takes the major. bson-calypso, kyo and rapid leave their 3.8 pin and join the LTS line.
  • WireCodec no longer derives itself for any case class it meets. A field whose type has no codec is a compile
    error naming the type; an opaque type keeps its representation. enum cases and sealed leaves still derive with
    their parent.
  • Driver exceptions arrive as MongoError. Match on MongoError, or on cause, which holds the original.
  • Compound PrimaryKeys are named tuples. compound(o => (userId = o.userId, seq = o.seq), snakeCase) replaces
    the positional tuple plus its parallel name list. compound3/compound4 removed.
  • Repository conditions say what they take: findBy → findByField, updateBy → updateByFilter,
    deleteBy → deleteByFilter, getBy → getByFilter.
  • Repository.insertOne returns K, insertMany returns List[K], not Option[BsonValue]/List[BsonValue].
  • getDirectCollection no longer takes a FieldNaming — the codec decides how queries spell field names.
  • The direct path reads numbers like the BsonValue path — a Long stored as Int32 now decodes; a wrong type
    is BsonError.TypeMismatch, not a raw driver exception.
  • aggregate[B] takes BsonDocumentDecoder[B], not a codec. Existing calls compile unchanged.
  • $min/$max take ComparableOf, so dates, strings and ObjectIds work. $inc/$mul stay numeric.
  • WatchOptions and RsBridgeConfig are final classes with withX builders. RsBridgeConfig.Default →
    .default; bufferSize must be positive.
  • Effect.traverse → Effect.flatTraverse. Effect gained monotonic with a default, so implementors are
    unaffected.
  • Streamable.instance is private[mongo4s]; mongo4s.bson.DecodeResult moved to mongo4s-bson-core;
    createCollection gained a defaulted options parameter.
  • withTransaction retries — TransientTransactionError restarts the transaction, UnknownTransactionCommitResult
    retries only the commit, both under one 120s deadline. No compile error behind this one;
    TransactionOptions.withoutRetries restores 2.x.

New features

  • MongoError — DuplicateKey, WriteConflict, ExecutionTimeout, Unauthorized, Unavailable,
    BulkWriteFailed, Failed. Translated on the Publisher, so every runtime reports the same type; cause,
    code, labels and hasLabel are all there. Costs nothing measurable on the happy path.
  • explain on find and aggregate, with ExplainVerbosity, plus ExplainSummary.of — indexes used,
    collection scanned, sorted in memory, execution counters. Reads both shapes a plan can take.
  • selectAs[K] — find(...).selectAs[(name: String, age: Int)] builds projection and decoder from one shape.
    A wrong label or type is a compile error. Works on direct collections, where partial reads were not possible.
  • aggregateDirect[B] — pipeline output through a WireCodec[B], AST-free on a direct collection.
  • Repository.findPage(limit, after, filter) — keyset paging over the primary key, lexicographic for compound
    keys. No re-scan, and deletions do not shift the rows still to come.
  • TransactionOptions — the retry window plus readConcern, writeConcern, readPreference, maxCommitTime.
  • Geospatial filters — near, nearSphere, within, intersects over GeoJSON plus legacy 2d shapes. An
    unclosed polygon ring is refused rather than sent.
  • $bucket, $densify, $setWindowFields, with typed boundaries and window vocabulary.
  • CreateCollectionOptions — capped, validators, time-series, clustered, expireAfter, collation. Combinations
    the server accepts and ignores are rejected.
  • Native WireCodec for BigDecimal, Instant, UUID, ObjectId — no BsonValue per field. Same BSON on
    the wire.
  • Repository.getByField/updateByField/deleteByField, each its …ByFilter counterpart over
    field.equalTo(value).
  • The fake simulates much more — aggregate ($match, $sort, $skip, $limit, $project, $count,
    $group) and distinct, so aggregating service code is unit-testable without Docker.
  • Sort.byTextScore, Projection.slice/sliceFrom, Index.text, Index.geo2D,
    WatchOptions.withExpandedEvents, ChangeEvent.wallTime/splitEvent, and key on
    Filter/Update/Stage/Accumulator.

Bug fixes

Each reproduced by a failing test first; the fake-versus-server ones are pinned by parity specs against a real
MongoDB, and each spec was run against the unfixed fake to confirm it catches the divergence.

  • The fake reported an upsert that inserted as doing nothing — no _id stamped, so wasUpserted was false
    where the server says true. The bulk path reported the entity's id field, or the document as JSON.
  • The fake simulated four of the update operators while the docs said updates were simulated. All are simulated
    now, $push's $each/$position/$slice/$sort included.
  • The fake answered several queries differently from the server — array filters compared the whole array,
    dotted paths through arrays found nothing, $regex anchored and ignored options, $eq null missed absent fields,
    $inc promoted Int32 to Int64, $count on no input returned a row.
  • PublisherIterator.cancel leaked a thread per cancelled or timed-out rapid stream — no terminal was queued,
    so a consumer parked in hasNext stayed parked.
  • RsBridge.unit wrapped failures in CompletionException on some backends, hiding the driver's exception type
    and the error labels transaction retries depend on.
  • selectAs reported MissingField for an absent Option field, where reading the whole entity gave None.
  • Repository.bulkWrite had no ordered flag and always batched, so an unordered bulk stopped at the first
    failing batch.
  • @publicInBinary on the accessors synthesized for Field.of and the WireCodec derivations — binary-unstable in
    a way MiMa cannot check.
  • ReadmeReferencesSpec fails if a README snippet names a member the source does not define.

Benchmarks

Re-measured end to end on stated hardware. Six JMH harnesses, mongo4cats as reference —
BENCHMARKS.md.

  • WireCodec to real BSON bytes: 2.7–3.2× faster than either mongo4cats codec, up to 6.9× less allocation.
  • Against a real MongoDB every single-document operation lands inside run-to-run error for all six stacks.
  • Bulk reads pull ahead: mongo4cats allocates 2.0–13× more on findAll/findStream.
  • The server-backed rates are bound by Docker's round trip, not by any library on the page — and the same benchmark
    on the same server moved 18% between two runs eight hours apart, so anything narrower is noise.

Compatibility

  • Scala 3.9 LTS, every module. mongo4s-kyo remains outside the binary compatibility promise while kyo is on a
    release candidate.
  • Building requires JDK 25 — the build stops and says so rather than failing on mongo4s-kyo with
    UnsupportedClassVersionError. Build-time only; published artifacts are unaffected.
  • Tested against JDK 25 and MongoDB 8.2. Note mongo:8 (the 8.0 line) will not start on Linux kernels 6.19+
    (SERVER-121912), which includes Docker Desktop's VM; 8.2 and
    7 both run.

v2.0.0

Choose a tag to compare

@shadowsmind shadowsmind released this 01 Sep 22:35

mongo4s 2.0.0

Two things drive it. The write API's options moved out of method parameters and into immutable values, which is what
lets every future option ship in a 2.x minor instead of another major. And an independent audit of the codebase
found a set of real defects, all fixed here, each reproduced by a failing test first.

The compiler catches every breaking change — none of them alters behaviour silently. The full migration guide is in
COMPATIBILITY.md.

libraryDependencies += "org.mongo4s" %% "mongo4s-core" % "2.0.0"

New features

Read and write concerns per database and collection. withReadConcern, withWriteConcern and
withReadPreference return a new handle rather than mutating the one you hold, so per-operation control is just
chaining: collection.withWriteConcern(WriteConcern.MAJORITY).insertOne(x). Previously these could only be set for
the whole client, and there was no way to build a MongoCollection from a driver collection you had narrowed
yourself.

mongo4s-testkit, a new published module. FakeMongoCollection used to live in mongo4s-repositories' test
sources, which meant nobody outside this build could use it. It is now published, together with a new
FakeRepository — a real BaseMongoRepository over a fake collection, so the repository logic under test is the one
that ships.

arrayFilters and the positional operators. $[] and $[identifier] are ordinary path segments, so
Field.of[Order, List[Item]](_.items) / "$[low]" / "qty" builds them; the filters themselves go in UpdateOptions.

Write, delete and count options. collation, hint and comment on every write, bypassDocumentValidation
where a whole document is written, and limit/skip/maxTime on count.

$each modifiers on pushAll — $slice, $sort, $position, for a capped, ordered array maintained by the
server rather than by the application.

More index types — hashed, 2dsphere, 2d, plus hidden, collation and wildcardProjection. Wildcard
indexes needed no new API: $** is a stored path.

Richer aggregation — $out and $merge take a target database, and $merge its on/whenMatched/
whenNotMatched policies; $lookup gained its sub-pipeline form with let; $graphLookup is new.

omitNoneFields (default on). A None field is left out of the document instead of written as an explicit
null, which is what a consumer asked for after reporting a 2×+ drop in memory footprint. Reads are unaffected in
both directions, so mixed collections keep working.

BsonTypeName is public and an enum, so hasType takes a value rather than a $type alias string that a typo
turns into a filter matching nothing.

API changes

Every item here is a compile error, not a behaviour change.

Write options are values, not parameters. UpdateOptions, ReplaceOptions, DeleteOptions, CountOptions and
the entity-typed FindOneAndUpdateOptions[A] / FindOneAndReplaceOptions[A] / FindOneAndDeleteOptions[A]:

collection.updateOne(filter, update, upsert = true)         // 1.x
collection.updateOne(filter, update, UpdateOptions.upsert)  // 2.0

deleteOne, deleteMany and count gained an options parameter. WriteCommand and Repository follow.

Projection is two types. Mixing inclusion with exclusion no longer compiles, where it used to silently discard
whatever you listed first and return more fields than you asked for. Projection.empty returns
Projection.Everything[E].

Index and WireCodecConfig are no longer case classes. Both are final classes with private constructors and
withX builders, for the same reason: a case class cannot gain a field without breaking apply/copy again in
every release, and options keep arriving.

hasType takes BsonTypeName, and BsonError.TypeMismatch carries it in both fields. The rendered message is
unchanged.

MongoCollection and MongoDatabase gained three abstract methods — the concern setters — which matters if you
implement either trait outside this library. Stage gained LookupPipeline and GraphLookup cases, which matters
if you match on it exhaustively.

FakeMongoCollection moved to mongo4s.testkit, in the new module.

Bug fixes

Each of these was reproduced by a failing test before being fixed; the codec and query fixes are pinned against a
real MongoDB.

  • A sealed hierarchy stored as the root entity of a direct collection could be written but never read back. The
    decoder required the _type discriminator to be the document's first field, and the server stores _id first.
    Either and Ior carried their own copies of the same logic and the same defect.
  • Every write threw under an unacknowledged write concern. With w=0 the driver has no result to report, and
    each getter raised UnsupportedOperationException. Results now come back empty.
  • Decimal128 lost precision above 2^53 by being read through Double; the Long range check admitted exactly
    2^63 and saturated; a non-finite value threw ArithmeticException past the Either.
  • An explicit BsonEncoder/BsonDecoder pair and automatic WireCodec derivation were ambiguous, so a case
    class carrying both resolved no codec at all. Derivation is now the lower priority.
  • cats.data.Ior decoded its two sides by position, silently swapping them when the stored field order differed
    and both sides shared a wire type.
  • Sort and Index kept a repeated field's original position, so asc(a).asc(b).desc(a) sorted by a first.
  • 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 projection that mixed inclusion and exclusion silently returned extra fields, and Projection.excludeId
    followed by include silently lost the _id exclusion.
  • WatchOptions' three starting points were not mutually exclusive, so a resume token and an operation time
    could both reach the server, which rejects that.
  • A bare discriminator naming a case that carries fields raised IndexOutOfBoundsException instead of a
    BsonError, and 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, where every other runtime raises it.
  • kyo's stream failed as a ClassCastException when handed a Streamable it had not derived itself.
  • Reads on the direct path are strict about BSON numeric types — an Int32 where the model says Long is an
    error, unlike the lenient getCollection path. This was always true; it is now pinned by tests and documented.

Known gaps

GridFS, geospatial operators, CreateCollectionOptions, transaction retry semantics and a circe bridge are not
covered. Each has a workaround today, and all but one are additive — see
ROADMAP.md.

Compatibility

Scala 3.3 LTS, except mongo4s-bson-calypso, mongo4s-kyo and mongo4s-rapid, which are pinned to 3.8 by their
upstream dependencies and cannot be consumed from a 3.3 project. mongo4s-kyo sits outside the binary compatibility
promise while kyo is on a release candidate. Tested against JDK 25 and MongoDB 7.

v1.1.0

Choose a tag to compare

@shadowsmind shadowsmind released this 31 Aug 18:55

Highlights

  • bson-direct no longer stores null for absent Option fields. A field holding None is left out of the document entirely, so collections with many optional fields get materially smaller on disk. Reads are unaffected in both directions — a missing field decodes to None, and documents already carrying an explicit null still decode to None — so there is no migration.
  • WireCodecConfig is now a builder: start from WireCodecConfig.Default or .SnakeCase and chain withFieldNaming, withDiscriminatorNaming, withEncodeEmptyCasesAsString, withOmitNoneFields. Derivation options can now be added without breaking binary compatibility, so this is the last time the type changes shape.
  • Binary compatibility is enforced, not just promised. MiMa runs in CI against the previous release; every deliberate exception is a filter in mima.sbt and an entry in COMPATIBILITY.md.
  • DESIGN.md — why the library is shaped the way it is: what each decision cost, which sharp edges are deliberate, and what was rejected. Benchmarks and the compatibility policy moved into their own files too.

Breaking changes

  • WireCodecConfig is no longer a case class … Replace WireCodecConfig(fieldNaming = x) with WireCodecConfig.Default.withFieldNaming(x). Source and binary — recompile against 1.1.0.
  • Documents written through bson-direct no longer contain keys for None fields. {field: null} still matches them, but $exists: true and sparse indexes no longer see them; withOmitNoneFields(false) returns the old behavior.