Skip to content

v5.1.0

Choose a tag to compare

@shadowsmind shadowsmind released this 02 Nov 16:11
· 7 commits to main since this release

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, not per document.

Compatibility

  • Binary compatible with 5.0.0. New members of Repository, MongoCollection and MongoDatabase carry bodies, so
    an implementation of your own keeps compiling.
  • Scala 3.9 LTS, with the integration tests compiled on 3.10; 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.