Skip to content

v3.0.0

Choose a tag to compare

@shadowsmind shadowsmind released this 11 Sep 08:01
· 167 commits to main since this release

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.